#!/usr/bin/perl -w
#
# debianqueued -- daemon for managing Debian upload queues
#
# Copyright (C) 1997 Roman Hodek <Roman.Hodek@informatik.uni-erlangen.de>
# Copyright (C) 2001-2007 Ryan Murray <rmurray@debian.org>
# Copyright (C) 2008 Thomas Viehmann <tv@beamnet.de>
#
# 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 or
# (at your option) any later version.
# This program comes with ABSOLUTELY NO WARRANTY!
#

require 5.002;
no lib '.';
use strict;
use List::Util;
use POSIX;
use POSIX qw( strftime sys_stat_h sys_wait_h signal_h );
use Net::Ping;
use Net::FTP;
use Socket qw( PF_INET AF_INET SOCK_STREAM );
use Config;
use Sys::Hostname;
use File::Copy;
use Digest::MD5;

setlocale(&POSIX::LC_ALL, "C");
$ENV{"LC_ALL"} = "C";

# ---------------------------------------------------------------------------
#								configuration
# ---------------------------------------------------------------------------

package conf;
( $conf::queued_dir = ( ( $0 !~ m,^/, ) ? POSIX::getcwd() . "/" : "" ) . $0 )
  =~ s,/[^/]+$,,;

# various programs:
our $gpg       = "/usr/bin/gpg";
our $ssh       = "/usr/bin/ssh";
our $scp       = "/usr/bin/scp";
our $ssh_agent = "/usr/bin/ssh-agent";
our $ssh_add   = "/usr/bin/ssh-add";
our $mail      = "/usr/sbin/sendmail";

# default umask:
#   This is mostly for the "copy" upload method.  Logs, pidfile get
#   explicit permissions via `chmod`.
our $umask     = 0022;

require "$conf::queued_dir/config";
my $junk = $conf::debug;    # avoid spurious warnings about unused vars
$junk = $conf::ssh_key_file;
$junk = $conf::stray_remove_timeout;
$junk = $conf::problem_report_timeout;
$junk = $conf::queue_delay;
$junk = $conf::keep_files;
$junk = $conf::valid_files;
$junk = $conf::max_upload_retries;
$junk = $conf::upload_delay_1;
$junk = $conf::upload_delay_2;
$junk = $conf::check_md5sum;

$junk         = $conf::ftpdebug;
$junk         = $conf::ftptimeout;
$junk         = @conf::nonus_packages;
$junk         = @conf::maintainer_mail;
$junk         = @conf::targetdir_delayed;
$junk         = $conf::mail ||= '/usr/sbin/sendmail';
$junk         = $conf::overridemail;
$conf::target = "localhost" if $conf::upload_method eq "copy";

package main;

if (defined $conf::umask) {
  umask $conf::umask;
}

( $main::progname = $0 ) =~ s,.*/,,;

($main::hostname, undef, undef, undef, undef) = gethostbyname(hostname());

my %packages = ();
my $re_file_safe_prefix = qr/\A([a-zA-Z0-9.][a-zA-Z0-9_.:~+-]*)/s;
my $re_file_safe = qr/$re_file_safe_prefix\z/s;

# extract -r and -k args
$main::arg = "";
if ( @ARGV == 1 && $ARGV[0] =~ /^-[rk]$/ ) {
  $main::arg = ( $ARGV[0] eq '-k' ) ? "kill" : "restart";
  shift @ARGV;
}

# test for another instance of the queued already running
my ( $pid, $delayed_dirs, $adelayedcore );
if ( open( PIDFILE, "<", $conf::pidfile ) ) {
  chomp( $pid = <PIDFILE> );
  close(PIDFILE);
  if ( !$pid ) {

    # remove stale pid file
    unlink($conf::pidfile);
  } elsif ($main::arg) {
    local ($|) = 1;
    print "Killing running daemon (pid $pid) ...";
    kill( 15, $pid );
    my $cnt = 20;
    while ( kill( 0, $pid ) && $cnt-- > 0 ) {
      sleep 1;
      print ".";
    }
    if ( kill( 0, $pid ) ) {
      print " failed!\nProcess $pid still running.\n";
      exit 1;
    }
    print "ok\n";
    if ( -e "$conf::incoming/core" ) {
      unlink("$conf::incoming/core");
      print "(Removed core file)\n";
    }
    for ( $delayed_dirs = 0 ;
          $delayed_dirs <= $conf::max_delayed ;
          $delayed_dirs++ )
    {
      $adelayedcore =
        sprintf( "$conf::incoming_delayed/core", $delayed_dirs );
      if ( -e $adelayedcore ) {
        unlink($adelayedcore);
        print "(Removed core file)\n";
      }
    } ## end for ( $delayed_dirs = 0...
    exit 0 if $main::arg eq "kill";
  } else {
    die "Another $main::progname is already running (pid $pid)\n"
      if $pid && kill( 0, $pid );
  }
} elsif ( $main::arg eq "kill" ) {
  die "No daemon running\n";
} elsif ( $main::arg eq "restart" ) {
  print "(No daemon running; starting anyway)\n";
}

# if started without arguments (initial invocation), then fork
if ( !@ARGV ) {

  # now go to background
  die "$main::progname: fork failed: $!\n"
    unless defined( $pid = fork );
  if ($pid) {

    # parent: wait for signal from child (SIGCHLD or SIGUSR1) and exit
    my $sigset = POSIX::SigSet->new();
    $sigset->emptyset();
    $SIG{"CHLD"} = sub { };
    $SIG{"USR1"} = sub { };
    POSIX::sigsuspend($sigset);
    waitpid( $pid, WNOHANG );
    if ( kill( 0, $pid ) ) {
      print "Daemon (on $main::hostname) started in background (pid $pid)\n";
      exit 0;
    } else {
      exit 1;
    }
  } else {

    # child
    setsid;
    if ( $conf::upload_method eq "ssh" ) {

      # exec an ssh-agent that starts us again
      # force shell to be /bin/sh, ssh-agent may base its decision
      # whether to use a fd or a Unix socket on the shell...
      $ENV{"SHELL"} = "/bin/sh";
      exec $conf::ssh_agent, $0, "startup", getppid();
      die "$main::progname: Could not exec $conf::ssh_agent: $!\n";
    } else {

      # no need to exec, just set up @ARGV as expected below
      @ARGV = ( "startup", getppid() );
    }
  } ## end else [ if ($pid)
} ## end if ( !@ARGV )
die "Please start without any arguments.\n"
  if @ARGV != 2 || $ARGV[0] ne "startup";
my $parent_pid = $ARGV[1];

do {
  my $version;
  ( $version = 'Release: 0.95' ) =~ s/\$ ?//g;
  print "debianqueued $version\n";
};

# check if all programs exist
my $prg;
foreach $prg ( $conf::gpg, $conf::ssh, $conf::scp, $conf::ssh_agent,
               $conf::ssh_add, $conf::mail )
{
  die "Required program $prg doesn't exist or isn't executable\n"
    if !-x $prg;

  # check for correct upload method
  die "Bad upload method '$conf::upload_method'.\n"
    if $conf::upload_method ne "ssh"
      && $conf::upload_method ne "ftp"
      && $conf::upload_method ne "copy";
  die "No keyrings\n" if !@conf::keyrings;

} ## end foreach $prg ( $conf::gpg, ...
die "statusfile path must be absolute."
  if $conf::statusfile !~ m,^/,;
die "upload and target queue paths must be absolute."
  if $conf::incoming !~ m,^/,
    || $conf::incoming_delayed !~ m,^/,
    || $conf::targetdir !~ m,^/,
    || $conf::targetdir_delayed !~ m,^/,;

# ---------------------------------------------------------------------------
#							   initializations
# ---------------------------------------------------------------------------

# prototypes
sub calc_delta();
sub check_dir();
sub get_filelist_from_known_good_changes($);
sub age_delayed_queues();
sub process_changes($\@);
sub process_commands($);
sub age_delayed_queues();
sub is_on_target($\@);
sub copy_to_target(@);
sub pgp_check($);
sub check_alive(;$);
sub check_incoming_writable();
sub fork_statusd();
sub write_status_file();
sub print_status($$$$$$);
sub format_status_num(\$$);
sub format_status_str(\$$);
sub send_status();
sub ftp_open();
sub ftp_cmd($@);
sub ftp_close();
sub ftp_response();
sub ftp_code();
sub ftp_error();
sub ssh_cmd($);
sub scp_cmd(@);
sub check_alive(;$);
sub check_incoming_writable();
sub rm(@);
sub md5sum($);
sub msg($@);
sub debug(@);
sub init_mail(;$);
sub finish_mail();
sub send_mail($$$);
sub try_to_get_mail_addr($$);
sub format_time();
sub print_time($);
sub block_signals();
sub unblock_signals();
sub close_log($);
sub kid_died($);
sub restart_statusd();
sub fatal_signal($);

$ENV{"PATH"} = "/bin:/usr/bin";
$ENV{"IFS"} = "" if defined( $ENV{"IFS"} && $ENV{"IFS"} ne "" );

# constants for stat
sub ST_DEV()   { 0 }
sub ST_INO()   { 1 }
sub ST_MODE()  { 2 }
sub ST_NLINK() { 3 }
sub ST_UID()   { 4 }
sub ST_GID()   { 5 }
sub ST_RDEV()  { 6 }
sub ST_SIZE()  { 7 }
sub ST_ATIME() { 8 }
sub ST_MTIME() { 9 }
sub ST_CTIME() { 10 }

# fixed lengths of data items passed over status pipe
sub STATNUM_LEN() { 30 }
sub STATSTR_LEN() { 128 }

# init list of signals
defined $Config{sig_name}
  or die "$main::progname: No signal list defined!\n";
my $i = 0;
my $name;
foreach $name ( split( ' ', $Config{sig_name} ) ) {
  $main::signo{$name} = $i++;
}

@main::fatal_signals = qw( INT QUIT ILL TRAP ABRT BUS FPE USR2 SEGV PIPE
  TERM XCPU XFSZ PWR );

$main::block_sigset = POSIX::SigSet->new;
$main::block_sigset->addset( $main::signo{"INT"} );
$main::block_sigset->addset( $main::signo{"TERM"} );

# some constant net stuff
$main::tcp_proto = ( getprotobyname('tcp') )[2]
  or die "Cannot get protocol number for 'tcp'\n";
my $used_service = ( $conf::upload_method eq "ssh" ) ? "ssh" : "ftp";
$main::echo_port = ( getservbyname( $used_service, 'tcp' ) )[2]
  or die "Cannot get port number for service '$used_service'\n";

# clear queue of stored mails
@main::stored_mails = ();

# run ssh-add to bring the key into the agent (will use stdin/stdout)
if ( $conf::upload_method eq "ssh" ) {
  system "$conf::ssh_add $conf::ssh_key_file"
    and die "$main::progname: Running $conf::ssh_add failed "
    . "(exit status ", $? >> 8, ")\n";
}

# change to queue dir
chdir($conf::incoming)
  or die "$main::progname: cannot cd to $conf::incoming: $!\n";

# needed before /dev/null redirects, some system send a SIGHUP when loosing
# the controlling tty
$SIG{"HUP"} = "IGNORE";

# open logfile, make it unbuffered
open( LOG, ">>", $conf::logfile )
  or die "Cannot open my logfile $conf::logfile: $!\n";
chmod( 0644, $conf::logfile )
  or die "Cannot set modes of $conf::logfile: $!\n";
select( ( select(LOG), $| = 1 )[0] );

sleep(1);
$SIG{"HUP"} = \&close_log;

# redirect stdin, ... to /dev/null
open( STDIN, "<", "/dev/null" )
  or die "$main::progname: Can't redirect stdin to /dev/null: $!\n";
open( STDOUT, ">&", \*LOG )
  or die "$main::progname: Can't redirect stdout to $conf::logfile: $!\n";
open( STDERR, ">&", \*LOG )
  or die "$main::progname: Can't redirect stderr to $conf::logfile: $!\n";

# ok, from this point usually no "die" anymore, stderr is gone!
msg( "log", "daemon (pid $$) (on $main::hostname) started\n" );

# initialize variables used by send_status before launching the status daemon
$main::dstat = "i";
format_status_num( $main::next_run, time + 10 );
format_status_str( $main::current_changes, "" );
check_alive();
$main::incoming_writable = 1;    # assume this for now

# start the daemon watching the 'status' FIFO
if ( $conf::statusfile && $conf::statusdelay == 0 ) {
  $main::statusd_pid = fork_statusd();
  $SIG{"CHLD"}       = \&kid_died;       # watch out for dead status daemon
                                         # SIGUSR1 triggers status info
  $SIG{"USR1"}       = \&send_status;
} ## end if ( $conf::statusfile...
$main::maind_pid = $$;

END {
  kill( $main::signo{"ABRT"}, $$ )
    if defined $main::signo{"ABRT"};
}

# write the pid file
open( PIDFILE, ">", $conf::pidfile )
  or msg( "log", "Can't open $conf::pidfile: $!\n" );
printf PIDFILE "%5d\n", $$;
close(PIDFILE);
chmod( 0644, $conf::pidfile )
  or die "Cannot set modes of $conf::pidfile: $!\n";

# other signals will just log an error and exit
foreach (@main::fatal_signals) {
  $SIG{$_} = \&fatal_signal;
}

# send signal to user-started process that we're ready and it can exit
kill( $main::signo{"USR1"}, $parent_pid );

# ---------------------------------------------------------------------------
#								 the mainloop
# ---------------------------------------------------------------------------

# default to classical incoming/target
$main::current_incoming  = $conf::incoming;
$main::current_targetdir = $conf::targetdir;

$main::dstat = "i";
write_status_file() if $conf::statusdelay;
while (1) {

  # ping target only if there is the possibility that we'll contact it (but
  # also don't wait too long).
  my @have_changes = <*.changes *.commands *.dak-commands>;
  for ( my $delayed_dirs = 0 ;
        $delayed_dirs <= $conf::max_delayed ;
        $delayed_dirs++ )
  {
    my $adelayeddir = sprintf( "$conf::incoming_delayed", $delayed_dirs );
    push( @have_changes, <$adelayeddir/*.changes> );
  } ## end for ( my $delayed_dirs ...
  check_alive()
    if @have_changes || ( time - $main::last_ping_time ) > 8 * 60 * 60;

  if ( @have_changes && $main::target_up ) {
    check_incoming_writable if !$main::incoming_writable;
    check_dir() if $main::incoming_writable;
  }
  $main::dstat = "i";
  write_status_file() if $conf::statusdelay;

  if ( $conf::upload_method eq "copy" ) {
    age_delayed_queues();
  }

  # sleep() returns if we received a signal (SIGUSR1 for status FIFO), so
  # calculate the end time once and wait for it being reached.
  format_status_num( $main::next_run, time + $conf::queue_delay );
  my $delta;
  while ( ( $delta = calc_delta() ) > 0 ) {
    debug("mainloop sleeping $delta secs");
    sleep($delta);

    # check if statusd died, if using status FIFO, or update status file
    if ($conf::statusdelay) {
      write_status_file();
    } else {
      restart_statusd();
    }
  } ## end while ( ( $delta = calc_delta...
} ## end while (1)

sub calc_delta() {
  my $delta;

  $delta = $main::next_run - time;
  $delta = $conf::statusdelay
    if $conf::statusdelay && $conf::statusdelay < $delta;
  return $delta;
} ## end sub calc_delta()

# ---------------------------------------------------------------------------
#							main working functions
# ---------------------------------------------------------------------------

#
# main function for checking the incoming dir
#
sub check_dir() {
  my ( @files, @changes, @keep_files, @this_keep_files, @stats, $file,
       $adelay );

  debug("starting checkdir");
  $main::dstat = "c";
  write_status_file() if $conf::statusdelay;

  for ( $adelay = -1 ; $adelay <= $conf::max_delayed ; $adelay++ ) {
    if ( $adelay == -1 ) {
      $main::current_incoming       = $conf::incoming;
      $main::current_incoming_short = "";
      $main::current_targetdir      = $conf::targetdir;
    } else {
      $main::current_incoming = sprintf( $conf::incoming_delayed, $adelay );
      $main::current_incoming_short = sprintf( "DELAYED/%d-day", $adelay );
      $main::current_targetdir = sprintf( $conf::targetdir_delayed, $adelay );
    }

    # need to clear directory specific variables
    undef(@keep_files);
    undef(@this_keep_files);

    chdir($main::current_incoming)
      or (
           msg(
                "log",
                "Cannot change to dir "
                  . "${main::current_incoming_short}: $!\n"
              ),
           return
         );

    # look for *.commands and *.dak-commands files but not in delayed queues
    if ( $adelay == -1 ) {
      foreach $file (<*.commands>) {
        next unless $file =~ /$re_file_safe/;
        init_mail($file);
        block_signals();
        process_commands($file);
        unblock_signals();
        $main::dstat = "c";
        write_status_file() if $conf::statusdelay;
        finish_mail();
      } ## end foreach $file (<*.commands>)
	  foreach $file (<*.dak-commands>) {
		next unless $file =~ /$re_file_safe/;
		init_mail($file);
		block_signals();
		process_dak_commands($file);
		unblock_signals();
		$main::dstat = "c";
		write_status_file() if $conf::statusdelay;
		finish_mail();
	  }
    } ## end if ( $adelay == -1 )
    opendir( INC, "." )
      or (
           msg(
                "log", "Cannot open dir ${main::current_incoming_short}: $!\n"
              ),
           return
         );
    @files = readdir(INC);
    closedir(INC);

    # process all .changes files found
    @changes = grep /\.changes$/, @files;
    push( @keep_files, @changes );    # .changes files aren't stray
    foreach $file (@changes) {
      next unless $file =~ /$re_file_safe/;
      init_mail($file);

      # wrap in an eval to allow jumpbacks to here with die in case
      # of errors
      block_signals();
      eval { process_changes( $file, @this_keep_files ); };
      unblock_signals();
      msg( "log,mail", $@ ) if $@;
      $main::dstat = "c";
      write_status_file() if $conf::statusdelay;

      # files which are ok in conjunction with this .changes
      debug("$file tells to keep @this_keep_files");
      push( @keep_files, @this_keep_files );
      finish_mail();

      # break out of this loop if the incoming dir has become unwritable
      goto end_run if !$main::incoming_writable;
    } ## end foreach $file (@changes)
    ftp_close() if $conf::upload_method eq "ftp";

    # find files which aren't related to any .changes
    foreach $file (@files) {

      # filter out files we never want to delete
      next if !-f $file ||    # may have disappeared in the meantime
             $file eq "."
          || $file eq ".."
          || ( grep { $_ eq $file } @keep_files )
          || $file =~ /$conf::keep_files/;

      # Delete such files if they're older than
      # $stray_remove_timeout; they could be part of an
      # yet-incomplete upload, with the .changes still missing.
      # Cannot send any notification, since owner unknown.
      next if !( @stats = stat($file) );
      my $cmtime = List::Util::max($stats[ST_CTIME], $stats[ST_MTIME]);
      my $age = time - $cmtime;
      my ( $maint, $pattern, @job_files );
      if (    $file =~ /^junk-for-writable-test/
           || $file !~ m,$conf::valid_files,
           || $file !~ /$re_file_safe/
           || $age >= $conf::stray_remove_timeout )
      {
        msg( "log",
             "Deleted stray file ${main::current_incoming_short}/$file\n" )
          if rm($file);
      } else {
        debug(
"found stray file ${main::current_incoming_short}/$file, deleting in ",
          print_time( $conf::stray_remove_timeout - $age )
        );
      } ## end else [ if ( $file =~ /^junk-for-writable-test/...
    } ## end foreach $file (@files)
  } ## end for ( $adelay = -1 ; $adelay...
  chdir($conf::incoming);

end_run:
  $main::dstat = "i";
  write_status_file() if $conf::statusdelay;
} ## end sub check_dir()

sub get_filelist_from_known_good_changes($) {
  my $changes = shift;

  local (*CHANGES);
  my (@filenames);

  # parse the .changes file
  open( CHANGES, "<", $changes )
    or die "$changes: $!\n";
outer_loop: while (<CHANGES>) {
    if (/^Files:/i) {
      while (<CHANGES>) {
        redo outer_loop if !/^\s/;
        my @field = split(/\s+/);
        next if @field != 6;

        # forbid shell meta chars in the name, we pass it to a
        # subshell several times...
        $field[5] =~ /$re_file_safe/;
        if ( $1 ne $field[5] ) {
          msg( "log", "found suspicious filename $field[5]\n" );
          next;
        }
        push( @filenames, $field[5] );
      } ## end while (<CHANGES>)
    } ## end if (/^Files:/i)
  } ## end while (<CHANGES>)
  close(CHANGES);
  return @filenames;
} ## end sub get_filelist_from_known_good_changes($)

#
# process one .changes file
#
sub process_changes($\@) {
  my $changes   = shift;
  my $keep_list = shift;
  my (
       $pgplines,     @files,     @filenames,  @changes_stats,
       $failure_file, $retries,   $last_retry, $upload_time,
       $file,         $do_report, $ls_l,
       $errs,         $pkgname,   $signator,   $extralines
     );
  local (*CHANGES);
  local (*FAILS);

  format_status_str( $main::current_changes,
                     "$main::current_incoming_short/$changes" );
  $main::dstat = "c";
  $main::mail_addr = "";
  write_status_file() if $conf::statusdelay;

  @$keep_list = ();
  msg( "log", "processing ${main::current_incoming_short}/$changes\n" );

  # run PGP on the file to check the signature
  if ( !( $signator = pgp_check($changes) ) ) {
    msg(
       "log,mail",
       "$main::current_incoming_short/$changes has bad PGP/GnuPG signature!\n"
    );
    goto remove_only_changes;
  } elsif ( $signator eq "LOCAL ERROR" ) {

    # An error has appened when starting pgp... Don't process the file,
    # but also don't delete it
    debug(
"Can't PGP/GnuPG check $main::current_incoming_short/$changes -- don't process it for now"
    );
    return;
  } ## end elsif ( $signator eq "LOCAL ERROR")

  # parse the .changes file
  open( CHANGES, "<", $changes )
    or die "Cannot open ${main::current_incoming_short}/$changes: $!\n";
  $pgplines        = 0;
  $extralines      = 0;
  @files           = ();
outer_loop: while (<CHANGES>) {
    if (/^---+(BEGIN|END) PGP .*---+$/) {
      ++$pgplines;
      next;
    }
    if ( $pgplines < 1 or $pgplines >= 3 ) {
      $extralines++ if length $_ > 1;
      next;
    }
    if ($pgplines != 1) {
      next;
    }
    if (/^Maintainer:\s*/i) {
      chomp( $main::mail_addr = $' );
      $main::mail_addr = $1 if $main::mail_addr =~ /<([^>]*)>/;
    } elsif (/^Source:\s*/i) {
      chomp( $pkgname = $' );
      $pkgname =~ s/\s+$//;
      $main::packages{$pkgname}++;
    } elsif (/^Files:/i) {
      while (<CHANGES>) {
        redo outer_loop if !/^\s/;
        my @field = split(/\s+/);
        next if @field != 6;

        # forbid shell meta chars in the name, we pass it to a
        # subshell several times...
        $field[5] =~ /$re_file_safe/;
        if ( $1 ne $field[5] ) {
          msg( "log", "found suspicious filename $field[5]\n" );
          msg(
            "mail",
"File '$field[5]' mentioned in $main::current_incoming_short/$changes\n",
            "has bad characters in its name. Removed.\n"
          );
          rm( $field[5] );
          next;
        } ## end if ( $1 ne $field[5] )
        push(
              @files,
              {
                md5  => $field[1],
                size => $field[2],
                name => $field[5]
              }
            );
        push( @filenames, $field[5] );
        debug( "includes file $field[5], size $field[2], ", "md5 $field[1]" );
      } ## end while (<CHANGES>)
    } ## end elsif (/^Files:/i)
  } ## end while (<CHANGES>)
  close(CHANGES);

  # tell check_dir that the files mentioned in this .changes aren't stray,
  # we know about them somehow
  @$keep_list = @filenames;

  # some consistency checks
  if ( $extralines ) {
    msg( "log,mail",
"$main::current_incoming_short/$changes contained lines outside the pgp signed "
."part, cannot process\n" );
    goto remove_only_changes;
  } ## end if ( $extralines )
  if ( !$main::mail_addr ) {
    msg( "log,mail",
"$main::current_incoming_short/$changes doesn't contain a Maintainer: field; "
        . "cannot process\n" );
    goto remove_only_changes;
  } ## end if ( !$main::mail_addr)
  if ( $main::mail_addr !~ /^(buildd_\S+-\S+|\S+\@\S+\.\S+)/ ) {

    # doesn't look like a mail address, maybe only the name
    my ( $new_addr, @addr_list );
    if ( $new_addr = try_to_get_mail_addr( $main::mail_addr, \@addr_list ) ) {

      # substitute (unique) found addr, but give a warning
      msg(
           "mail",
           "(The Maintainer: field didn't contain a proper "
             . "mail address.\n"
         );
      msg(
           "mail",
           "Looking for `$main::mail_addr' in the Debian "
             . "keyring gave your address\n"
         );
      msg( "mail", "as unique result, so I used this.)\n" );
      msg( "log",
           "Substituted $new_addr for malformed " . "$main::mail_addr\n" );
      $main::mail_addr = $new_addr;
    } else {

      # not found or not unique: hold the job and inform queue maintainer
      my $old_addr = $main::mail_addr;
      $main::mail_addr = $conf::maintainer_mail;
      msg(
        "mail",
"The job ${main::current_incoming_short}/$changes doesn't have a correct email\n"
      );
      msg( "mail", "address in the Maintainer: field:\n" );
      msg( "mail", "  $old_addr\n" );
      msg( "mail", "A check for this in the Debian keyring gave:\n" );
      msg( "mail",
           @addr_list
           ? "  " . join( ", ", @addr_list ) . "\n"
           : "  nothing\n" );
      msg( "mail", "Please fix this manually\n" );
      msg(
        "log",
"Bad Maintainer: field in ${main::current_incoming_short}/$changes: $old_addr\n"
      );
      goto remove_only_changes;
    } ## end else [ if ( $new_addr = try_to_get_mail_addr...
  } ## end if ( $main::mail_addr ...
  if ( $pgplines < 3 ) {
    msg(
        "log,mail",
        "$main::current_incoming_short/$changes isn't signed with PGP/GnuPG\n"
       );
    msg( "log", "(uploader $main::mail_addr)\n" );
    goto remove_only_changes;
  } ## end if ( $pgplines < 3 )
  if ( !@files ) {
    msg( "log,mail",
       "$main::current_incoming_short/$changes doesn't mention any files\n" );
    msg( "log", "(uploader $main::mail_addr)\n" );
    goto remove_only_changes;
  } ## end if ( !@files )

  # check for packages that shouldn't be processed
  if ( grep( $_ eq $pkgname, @conf::nonus_packages ) ) {
    msg(
         "log,mail",
         "$pkgname is a package that must be uploaded "
           . "to nonus.debian.org\n"
       );
    msg( "log,mail", "instead of target.\n" );
    msg( "log,mail",
         "Job rejected and removed all files belonging " . "to it:\n" );
    msg( "log,mail", "  ", join( ", ", @filenames ), "\n" );
    rm( $changes, @filenames );
    return;
  } ## end if ( grep( $_ eq $pkgname...

  $failure_file = $changes . ".failures";
  $retries = $last_retry = 0;
  if ( -f $failure_file ) {
    open( FAILS, "<", $failure_file )
      or die "Cannot open $main::current_incoming_short/$failure_file: $!\n";
    my $line = <FAILS>;
    close(FAILS);
    ( $retries, $last_retry ) = ( $1, $2 )
      if $line =~ /^(\d+)\s+(\d+)$/;
    push( @$keep_list, $failure_file );
  } ## end if ( -f $failure_file )

  die "Cannot stat ${main::current_incoming_short}/$changes (??): $!\n"
    if !( @changes_stats = stat($changes) );

  # Make $upload_time the maximum of all modification times of files
  # related to this .changes (and the .changes it self). This is the
  # last time something changes to these files.
  $upload_time = $changes_stats[ST_MTIME];
  for $file (@files) {
    my @stats;
    next if !( @stats = stat( $file->{"name"} ) );
    $file->{"stats"} = \@stats;
    $upload_time = $stats[ST_MTIME] if $stats[ST_MTIME] > $upload_time;
  } ## end for $file (@files)

  $do_report = ( time - $upload_time ) > $conf::problem_report_timeout;

  # now check all files for correct size and md5 sum
  for $file (@files) {
    my $filename = $file->{"name"};
    if ( !defined( $file->{"stats"} ) ) {

      # could be an upload that isn't complete yet, be quiet,
      # but don't process the file;
      msg( "log", "$filename doesn't exist (ignored for now)\n" );
      ++$errs;
    } elsif ( $file->{"stats"}->[ST_SIZE] < $file->{"size"}
              && !$do_report )
    {

      # could be an upload that isn't complete yet, be quiet,
      # but don't process the file
      msg( "log", "$filename is too small (ignored for now)\n" );
      ++$errs;
    } elsif ( $file->{"stats"}->[ST_SIZE] != $file->{"size"} ) {
      msg( "log,mail", "$filename has incorrect size; deleting it\n" );
      rm($filename);
      ++$errs;
    } elsif ( md5sum($filename) ne $file->{"md5"} ) {
      msg( "log,mail",
           "$filename has incorrect md5 checksum; ",
           "deleting it\n" );
      rm($filename);
      ++$errs;
    } ## end elsif ( md5sum($filename)...
  } ## end for $file (@files)

  if ($errs) {
    if ( ( time - $upload_time ) > $conf::bad_changes_timeout ) {

      # if a .changes fails for a really long time (several days
      # or so), remove it and all associated files
      msg(
          "log,mail",
          "$main::current_incoming_short/$changes couldn't be processed for ",
          int( $conf::bad_changes_timeout / ( 60 * 60 ) ),
          " hours and is now deleted\n"
         );
      msg( "log,mail", "All files it mentions are also removed:\n" );
      msg( "log,mail", "  ", join( ", ", @filenames ), "\n" );
      rm( $changes, @filenames, $failure_file );
    }

    # else: be quiet

    return;
  } ## end if ($errs)

  # if this upload already failed earlier, wait until the delay requirement
  # is fulfilled
  if ( $retries > 0
       && ( time - $last_retry ) <
       ( $retries == 1 ? $conf::upload_delay_1 : $conf::upload_delay_2 ) )
  {
    msg( "log", "delaying retry of upload\n" );
    return;
  } ## end if ( $retries > 0 && (...

  return if !ftp_open();

  # check if the job is already present on target
  # (moved to here, to avoid bothering target as long as there are errors in
  # the job)
  if ( $ls_l = is_on_target( $changes, @filenames ) ) {
    msg(
      "log,mail",
"$main::current_incoming_short/$changes is already present on target host:\n"
    );
    msg( "log,mail", "$ls_l\n" );
    msg( "mail",
         "Either you already uploaded it, or someone else ",
         "came first.\n" );
    msg( "log,mail", "Job $changes removed.\n" );
    rm( $changes, @filenames, $failure_file );
    return;
  } ## end if ( $ls_l = is_on_target...

  # clear sgid bit before upload, scp would copy it to target. We don't need
  # it anymore, we know there are no problems if we come here. Also change
  # mode of files to 644 if this should be done locally.
  $changes_stats[ST_MODE] &= ~S_ISGID;
  if ( !$conf::chmod_on_target ) {
    $changes_stats[ST_MODE] &= ~0777;
    $changes_stats[ST_MODE] |= 0644;
  }
  chmod +( $changes_stats[ST_MODE] ), $changes;

  # try uploading to target
  if ( !copy_to_target( $changes, @filenames ) ) {

    # if the upload failed, increment the retry counter and remember the
    # current time; both things are written to the .failures file. Don't
    # increment the fail counter if the error was due to incoming
    # unwritable.
    return if !$main::incoming_writable;
    if ( ++$retries >= $conf::max_upload_retries ) {
      msg( "log,mail",
           "$changes couldn't be uploaded for $retries times now.\n" );
      msg( "log,mail",
           "Giving up and removing it and its associated files:\n" );
      msg( "log,mail", "  ", join( ", ", @filenames ), "\n" );
      rm( $changes, @filenames, $failure_file );
    } else {
      $last_retry = time;
      if ( open( FAILS, ">", $failure_file ) ) {
        print FAILS "$retries $last_retry\n";
        close(FAILS);
        chmod( 0600, $failure_file )
          or die "Cannot set modes of $failure_file: $!\n";
      } ## end if ( open( FAILS, ">$failure_file"...
      push( @$keep_list, $failure_file );
      debug("now $retries failed uploads");
      msg(
           "mail",
           "The upload will be retried in ",
           print_time(
                         $retries == 1
                       ? $conf::upload_delay_1
                       : $conf::upload_delay_2
                     ),
           "\n"
         );
    } ## end else [ if ( ++$retries >= $conf::max_upload_retries)
    return;
  } ## end if ( !copy_to_target( ...

  # If the files were uploaded ok, remove them
  rm( $changes, @filenames, $failure_file );

  msg( "mail", "$changes uploaded successfully to $conf::target\n" );
  msg( "mail", "along with the files:\n  ", join( "\n  ", @filenames ),
       "\n" );
  msg( "log",
       "$changes processed successfully (uploader $main::mail_addr)\n" );

  return;

  remove_only_changes:
  msg(
    "log,mail",
    "Removing $main::current_incoming_short/$changes, but keeping its "
    . "associated files for now.\n"
    );
  rm($changes);
  return;

  # Check for files that have the same stem as the .changes (and weren't
  # mentioned there) and delete them. It happens often enough that people
  # upload a .orig.tar.gz where it isn't needed and also not in the
  # .changes. Explicitly deleting it (and not waiting for the
  # $stray_remove_timeout) reduces clutter in the queue dir and maybe also
  # educates uploaders :-)

  #	my $pattern = debian_file_stem( $changes );
  #	my $spattern = substr( $pattern, 0, -1 ); # strip off '*' at end
  #	my @other_files = glob($pattern);
  # filter out files that have a Debian revision at all and a different
  # revision. Those belong to a different upload.
  #	if ($changes =~ /^\Q$spattern\E-([\d.+-]+)/) {
  #		my $this_rev = $1;
  #		@other_files = grep( !/^\Q$spattern\E-([\d.+-]+)/ || $1 eq $this_rev,
  #							 @other_files);
  #}
  # Also do not remove those files if a .changes is among them. Then there
  # is probably a second upload for another version or another architecture.
  #	if (@other_files && !grep( /\.changes$/, @other_files )) {
  #		rm( @other_files );
  #		msg( "mail", "\nThe following file(s) seemed to belong to the same ".
  #					 "upload, but weren't listed\n" );
  #		msg( "mail", "in the .changes file:\n  " );
  #		msg( "mail", join( "\n  ", @other_files ), "\n" );
  #		msg( "mail", "They have been deleted.\n" );
  #		msg( "log", "Deleted files in upload not in $changes: @other_files\n" );
  #}
} ## end sub process_changes($\@)

#
# process one .dak-commands file
#
sub process_dak_commands {
  my $commands = shift;

  msg("log", "processing ${main::current_incoming_short}/$commands\n");

  # TODO: get mail address from signed contents
  # and NOT implement a third parser for armored PGP...
  $main::mail_addr = undef;

  # check signature
  my $signator = pgp_check($commands);
  if (!$signator) {
	msg("log,mail",
	    "$main::current_incoming_short/$commands has bad PGP/GnuPG signature!\n");
	msg("log,mail",
		"Removing $main::current_incoming_short/$commands\n");
	rm($commands);
	return;
  }
  elsif ($signator eq 'LOCAL ERROR') {
	debug("Can't check signature for $main::current_incoming_short/$commands -- don't process it for now");
	return;
  }
  msg("log,mail", "(PGP/GnuPG signature by $signator)\n");

  return if !ftp_open();

  # check target
  my @filenames = ($commands);
  if (my $ls_l = is_on_target($commands, @filenames)) {
	msg("log,mail", "$main::current_incoming_short/$commands is already present on target host:\n");
	msg("log,mail", "$ls_l\n");
	msg("log,mail", "Job $commands removed.\n");
	rm($commands);
	return;
  }

  if (!copy_to_target($commands)) {
	msg("log,mail", "$commands couldn't be uploaded to target.\n");
	msg("log,mail", "Giving up and removing it.\n");
	rm($commands);
	return;
  }

  rm($commands);
  msg("mail", "$commands uploaded successfully to $conf::target\n");
}

#
# process one .commands file
#
sub process_commands($) {
  my $commands = shift;
  my ( @cmds, $cmd, $pgplines, $signator );
  local (*COMMANDS);
  my ($file, @removed, $target_delay );

  format_status_str( $main::current_changes, $commands );
  $main::dstat = "c";
  $main::mail_addr = "";
  write_status_file() if $conf::statusdelay;

  msg( "log", "processing $main::current_incoming_short/$commands\n" );

  # run PGP on the file to check the signature
  if ( !( $signator = pgp_check($commands) ) ) {
    msg(
      "log,mail",
      "$main::current_incoming_short/$commands has bad PGP/GnuPG signature!\n"
    );
    goto remove;
  } elsif ( $signator eq "LOCAL ERROR" ) {

    # An error has appened when starting pgp... Don't process the file,
    # but also don't delete it
    debug(
"Can't PGP/GnuPG check $main::current_incoming_short/$commands -- don't process it for now"
    );
    return;
  } ## end elsif ( $signator eq "LOCAL ERROR")
  msg( "log", "(PGP/GnuPG signature by $signator)\n" );

  # parse the .commands file
  if ( !open( COMMANDS, "<", $commands ) ) {
    msg( "log", "Cannot open $main::current_incoming_short/$commands: $!\n" );
    return;
  }
  $pgplines        = 0;
  @cmds            = ();
outer_loop: while (<COMMANDS>) {
    if (/^---+(BEGIN|END) PGP .*---+$/) {
      ++$pgplines;
      next;
    }
    if ($pgplines != 1) {
      next;
    }
    if (/^Uploader:\s*/i) {
      chomp( $main::mail_addr = $' );
      $main::mail_addr = $1 if $main::mail_addr =~ /<([^>]*)>/;
    } elsif (/^Commands:/i) {
      $_ = $';
      for ( ; ; ) {
        s/^\s*(.*)\s*$/$1/;    # delete whitespace at both ends
        if ( !/^\s*$/ ) {
          push( @cmds, $_ );
          debug("includes cmd $_");
        }
        last outer_loop if !defined( $_ = scalar(<COMMANDS>) );
        chomp;
        redo outer_loop if !/^\s/ || /^$/;
      } ## end for ( ; ; )
    } ## end elsif (/^Commands:/i)
  } ## end while (<COMMANDS>)
  close(COMMANDS);

  # some consistency checks
  if ( !$main::mail_addr || $main::mail_addr !~ /^\S+\@\S+\.\S+/ ) {
    msg( "log,mail",
"$main::current_incoming_short/$commands contains no or bad Uploader: field: "
        . "$main::mail_addr\n" );
    msg( "log,mail",
         "cannot process $main::current_incoming_short/$commands\n" );
    $main::mail_addr = "";
    goto remove;
  } ## end if ( !$main::mail_addr...
  msg( "log", "(command uploader $main::mail_addr)\n" );

  if ( $pgplines < 3 ) {
    msg(
       "log,mail",
       "$main::current_incoming_short/$commands isn't signed with PGP/GnuPG\n"
    );
    msg(
      "mail",
      "or the uploaded file is broken. Make sure to transfer in binary mode\n"
    );
    msg( "mail", "or better yet - use dcut for commands files\n" );
    goto remove;
  } ## end if ( $pgplines < 3 )

  # now process commands
  msg(
    "mail",
"Log of processing your commands file $main::current_incoming_short/$commands:\n\n"
  );
  foreach $cmd (@cmds) {
    my @word = split( /\s+/, $cmd );
    msg( "mail,log", "> @word\n" );
    my $selecteddelayed = -1;
    next if @word < 1;

    if ( $word[0] eq "rm" ) {
      my @files = ();
      foreach ( @word[ 1 .. $#word ] ) {
        my $origword = $_;
        if (m,^DELAYED/([0-9]+)-day/,) {
          $selecteddelayed = $1;
          s,^DELAYED/[0-9]+-day/,,;
        }
        if (m,(^|/)\*,) {
          msg("mail,log", "$_: filename component cannot start with a wildcard\n");
        } elsif ( $origword eq "--searchdirs" ) {
          $selecteddelayed = -2;
        } elsif (m,/,) {
          msg(
            "mail,log",
"$_: filename may not contain slashes except for DELAYED/#-day/ prefixes\n"
          );
        } else {

          # process wildcards but also plain names
          my (@thesefiles);
          my $pat = quotemeta($_);
          $pat =~ s/\\\*/.*/g;
          $pat =~ s/\\\?/.?/g;
          $pat =~ s/\\([][])/$1/g;

          if ( $selecteddelayed < 0 ) {    # scanning or explicitly incoming
            opendir( DIR, "." );
            push( @thesefiles, grep /^$pat$/, readdir(DIR) );
            closedir(DIR);
          }
          if ( $selecteddelayed >= 0 ) {
            my $dir = sprintf( $conf::incoming_delayed, $selecteddelayed );
            opendir( DIR, $dir );
            push( @thesefiles,
                  map ( "$dir/$_", grep /^$pat$/, readdir(DIR) ) );
            closedir(DIR);
          } elsif ( $selecteddelayed == -2 ) {
            for ( my ($adelay) = 0 ;
                  ( !@thesefiles ) && $adelay <= $conf::max_delayed ;
                  $adelay++ )
            {
              my $dir = sprintf( $conf::incoming_delayed, $adelay );
              opendir( DIR, $dir );
              push( @thesefiles,
                    map ( "$dir/$_", grep /^$pat$/, readdir(DIR) ) );
              closedir(DIR);
            } ## end for ( my ($adelay) = 0 ...
          } ## end elsif ( $selecteddelayed ...
          push( @files, @thesefiles );
          if ( !@thesefiles ) {
            msg( "mail,log", "$origword did not match anything\n" );
          }
        } ## end else [ if ( $origword eq "--searchdirs")
      } ## end foreach ( @word[ 1 .. $#word...
      if ( !@files ) {
        msg( "mail,log", "No files to delete\n" );
      } else {
        @removed = ();
        foreach $file (@files) {
          if ( !-f $file ) {
            msg( "mail,log", "$file: no such file\n" );
          } elsif ( $file =~ /$conf::keep_files/ ) {
            msg( "mail,log", "$file is protected, cannot " . "remove\n" );
          } elsif ( !unlink($file) ) {
            msg( "mail,log", "$file: rm: $!\n" );
          } else {
            $file =~ s,$conf::incoming/?,,;
            push( @removed, $file );
          }
        } ## end foreach $file (@files)
        msg( "mail,log", "Files removed: @removed\n" ) if @removed;
      } ## end else [ if ( !@files )
    } elsif ( $word[0] eq "reschedule" ) {
      if ( @word != 3 ) {
        msg( "mail,log", "Wrong number of arguments\n" );
      } elsif ( $conf::upload_method ne "copy" ) {
        msg( "mail,log", "reschedule not available\n" );
      } elsif ( $word[1] =~ m,/, || $word[1] !~ m/\.changes/ ) {
        msg(
           "mail,log",
           "$word[1]: filename may not contain slashes and must be .changes\n"
        );
      } elsif ( !( ($target_delay) = $word[2] =~ m,^([0-9]+)-day$, )
                || $target_delay > $conf::max_delayed )
      {
        msg(
          "mail,log",
"$word[2]: rescheduling target must be #-day with # between 0 and $conf::max_delayed (in particular, no '/' allowed)\n"
        );
      } elsif ( $word[1] =~ /$conf::keep_files/ ) {
        msg( "mail,log", "$word[1] is protected, cannot do stuff with it\n" );
      } else {
        my ($adelay);
        for ( $adelay = 0 ;
            $adelay <= $conf::max_delayed
            && !-f (
              sprintf( "$conf::targetdir_delayed", $adelay ) . "/$word[1]" ) ;
            $adelay++ )
        {
        } ## end for ( $adelay = 0 ; $adelay...
        if ( $adelay > $conf::max_delayed ) {
          msg( "mail,log", "$word[1] not found\n" );
        } elsif ( $adelay == $target_delay ) {
          msg( "mail,log", "$word[1] already is in $word[2]\n" );
        } else {
          my (@thesefiles);
          my ($dir) = sprintf( "$conf::targetdir_delayed", $adelay );
          my ($target_dir) =
            sprintf( "$conf::targetdir_delayed", $target_delay );
          push( @thesefiles, $word[1] );
          push( @thesefiles,
                get_filelist_from_known_good_changes("$dir/$word[1]") );
          for my $afile (@thesefiles) {
            if ( $afile =~ m/\.changes$/ ) {
              utime undef, undef, ("$dir/$afile");
            }
            if ( !move("$dir/$afile", "$target_dir/$afile") ) {
              msg( "mail,log", "move: $!\n" );
            } else {
              msg( "mail,log", "$afile moved to $target_delay-day\n" );
            }
          } ## end for my $afile (@thesefiles)
        } ## end else [ if ( $adelay > $conf::max_delayed)
      } ## end else [ if ( @word != 3 )
    } elsif ( $word[0] eq "cancel" ) {
      if ( @word != 2 ) {
        msg( "mail,log", "Wrong number of arguments\n" );
      } elsif ( $conf::upload_method ne "copy" ) {
        msg( "mail,log", "cancel not available\n" );
      } elsif (
          $word[1] !~ m,$re_file_safe_prefix\.changes\z, )
      {
        msg( "mail,log",
          "argument to cancel must be one .changes filename without path\n" );
      } ## end elsif ( $word[1] !~ ...
      my (@files) = ();
      for ( my ($adelay) = 0 ; $adelay <= $conf::max_delayed ; $adelay++ ) {
        my ($dir) = sprintf( "$conf::targetdir_delayed", $adelay );
        if ( -f "$dir/$word[1]" ) {
          @removed = ();
          push( @files, "$word[1]" );
          push( @files,
                get_filelist_from_known_good_changes("$dir/$word[1]") );
          foreach $file (@files) {
            if ( !-f "$dir/$file" ) {
              msg( "mail,log", "$dir/$file: no such file\n" );
            } elsif ( "$dir/$file" =~ /$conf::keep_files/ ) {
              msg( "mail,log",
                   "$dir/$file is protected, cannot " . "remove\n" );
            } elsif ( !unlink("$dir/$file") ) {
              msg( "mail,log", "$dir/$file: rm: $!\n" );
            } else {
              push( @removed, $file );
            }
          } ## end foreach $file (@files)
          msg( "mail,log", "Files removed from $adelay-day: @removed\n" )
            if @removed;
        } ## end if ( -f "$dir/$word[1]")
      } ## end for ( my ($adelay) = 0 ...
      if ( !@files ) {
        msg( "mail,log", "No upload found: $word[1]\n" );
      }
    } else {
      msg( "mail,log", "unknown command $word[0]\n" );
    }
  } ## end foreach $cmd (@cmds)
  rm($commands);
  msg( "log",
       "-- End of $main::current_incoming_short/$commands processing\n" );
  return;

  remove:
  msg("log,mail", "Removing $main::current_incoming_short/$commands\n");
  rm($commands);
  return;
} ## end sub process_commands($)

sub age_delayed_queues() {
  for ( my ($adelay) = 0 ; $adelay <= $conf::max_delayed ; $adelay++ ) {
    my ($dir) = sprintf( "$conf::targetdir_delayed", $adelay );
    my ($target_dir);
    if ( $adelay == 0 ) {
      $target_dir = $conf::targetdir;
    } else {
      $target_dir = sprintf( "$conf::targetdir_delayed", $adelay - 1 );
    }
    for my $achanges (<$dir/*.changes>) {
      my $mtime = ( stat($achanges) )[9];
      if ( $mtime + 24 * 60 * 60 <= time || $adelay == 0 ) {
        utime undef, undef, ($achanges);
        my @thesefiles = ( $achanges =~ m,.*/([^/]*), );
        push( @thesefiles, get_filelist_from_known_good_changes($achanges) );
        for my $afile (@thesefiles) {
          if ( !move("$dir/$afile", "$target_dir/$afile") ) {
            msg( "log", "move: $!\n" );
          } else {
            msg( "log", "$afile moved to $target_dir\n" );
          }
        } ## end for my $afile (@thesefiles)
      } ## end if ( $mtime + 24 * 60 ...
    } ## end for my $achanges (<$dir/*.changes>)
  } ## end for ( my ($adelay) = 0 ...
} ## end sub age_delayed_queues()

#
# check if a file is already on target
#
sub is_on_target($\@) {
  my $file     = shift;
  my $filelist = shift;
  my $msg;
  my $stat;

  if ( $conf::upload_method eq "ssh" ) {
    ( $msg, $stat ) = ssh_cmd("ls -l $file");
  } elsif ( $conf::upload_method eq "ftp" ) {
    my $err;
    ( $msg, $err ) = ftp_cmd( "dir", $file );
    if ($err) {
      $stat = 1;
      $msg  = $err;
    } elsif ( !$msg ) {
      $stat = 1;
      $msg  = "ls: no such file\n";
    } else {
      $stat = 0;
      $msg = join( "\n", @$msg );
    }
  } else {
    my @allfiles = ($file);
    push( @allfiles, @$filelist );
    $stat = 1;
    $msg  = "no such file";
    for my $afile (@allfiles) {
      if ( -f "$conf::targetdir/$afile" ) {
        $stat = 0;
        $msg  = "$afile";
      }
    } ## end for my $afile (@allfiles)
    for ( my ($adelay) = 0 ;
          $adelay <= $conf::max_delayed && $stat ;
          $adelay++ )
    {
      for my $afile (@allfiles) {
        if (
           -f ( sprintf( "$conf::targetdir_delayed", $adelay ) . "/$afile" ) )
        {
          $stat = 0;
          $msg = sprintf( "%d-day", $adelay ) . "/$afile";
        } ## end if ( -f ( sprintf( "$conf::targetdir_delayed"...
      } ## end for my $afile (@allfiles)
    } ## end for ( my ($adelay) = 0 ...
  } ## end else [ if ( $conf::upload_method...
  chomp($msg);
  debug("exit status: $stat, output was: $msg");

  return "" if $stat && $msg =~ /no such file/i;    # file not present
  msg( "log", "strange ls -l output on target:\n", $msg ), return ""
    if $stat || $@;    # some other error, but still try to upload

  # ls -l returned 0 -> file already there
  $msg =~ s/\s\s+/ /g;    # make multiple spaces into one, to save space
  return $msg;
} ## end sub is_on_target($\@)

#
# copy a list of files to target
#
sub copy_to_target(@) {
  my @files = @_;
  my ( @md5sum, @expected_files, $sum, $name, $msgs, $stat );

  $main::dstat = "u";
  write_status_file() if $conf::statusdelay;

  # copy the files
  if ( $conf::upload_method eq "ssh" ) {
    ( $msgs, $stat ) = scp_cmd(@files);
    goto err if $stat;
  } elsif ( $conf::upload_method eq "ftp" ) {
    my ( $rv, $file );
    if ( !$main::FTP_chan->cwd($main::current_targetdir) ) {
      msg( "log,mail",
           "Can't cd to $main::current_targetdir on $conf::target\n" );
      goto err;
    }
    foreach $file (@files) {
      ( $rv, $msgs ) = ftp_cmd( "put", $file );
      goto err if !$rv;
    }
  } else {
    for my $file (@files) {
      eval { File::Copy::copy($file, $main::current_targetdir) };
      if ($@) {
        $stat = 1;
        $msgs = $@;
        goto err;
      }
    }
  }

  # check md5sums or sizes on target against our own
  my $have_md5sums = 1;
  if ($conf::check_md5sum) {
    if ( $conf::upload_method eq "ssh" ) {
      ( $msgs, $stat ) = ssh_cmd("md5sum @files");
      goto err if $stat;
      @md5sum = split( "\n", $msgs );
    } elsif ( $conf::upload_method eq "ftp" ) {
      my ( $rv, $err, $file );
      foreach $file (@files) {
        ( $rv, $err ) = ftp_cmd( "quot", "site", "md5sum", $file );
        if ($err) {
          next if ftp_code() == 550;    # file not found
          if ( ftp_code() == 500 ) {    # unimplemented
            $have_md5sums = 0;
            goto get_sizes_instead;
          }
          $msgs = $err;
          goto err;
        } ## end if ($err)
        chomp( my $t = ftp_response() );
        push( @md5sum, $t );
      } ## end foreach $file (@files)
      if ( !$have_md5sums ) {
      get_sizes_instead:
        foreach $file (@files) {
          ( $rv, $err ) = ftp_cmd( "size", $file );
          if ($err) {
            next if ftp_code() == 550;    # file not found
            $msgs = $err;
            goto err;
          }
          push( @md5sum, "$rv $file" );
        } ## end foreach $file (@files)
      } ## end if ( !$have_md5sums )
    } else {
      for my $file (@files) {
        my $md5 = eval { md5sum("$main::current_targetdir/$file") };
        if ($@) {
          $msgs = $@;
          goto err;
        }
        push @md5sum, "$md5 $file" if $md5;
      }
    }

    @expected_files = @files;
    foreach (@md5sum) {
      chomp;
      ( $sum, $name ) = split;
      next if !grep { $_ eq $name } @files;    # a file we didn't upload??
      next if $sum eq "md5sum:";               # looks like an error message
      if (    ( $have_md5sums && $sum ne md5sum($name) )
           || ( !$have_md5sums && $sum != ( -s $name ) ) )
      {
        msg(
             "log,mail",
             "Upload of $name to $conf::target failed ",
             "(" . ( $have_md5sums ? "md5sum" : "size" ) . " mismatch)\n"
           );
        goto err;
      } ## end if ( ( $have_md5sums &&...

      # seen that file, remove it from expect list
      @expected_files = map { $_ eq $name ? () : $_ } @expected_files;
    } ## end foreach (@md5sum)
    if (@expected_files) {
      msg( "log,mail", "Failed to upload the files\n" );
      msg( "log,mail", "  ", join( ", ", @expected_files ), "\n" );
      msg( "log,mail", "(Not present on target after upload)\n" );
      goto err;
    } ## end if (@expected_files)
  } ## end if ($conf::check_md5sum)

  if ($conf::chmod_on_target) {

    # change file's mode explicitly to 644 on target
    if ( $conf::upload_method eq "ssh" ) {
      ( $msgs, $stat ) = ssh_cmd("chmod 644 @files");
      goto err if $stat;
    } elsif ( $conf::upload_method eq "ftp" ) {
      my ( $rv, $file );
      foreach $file (@files) {
        ( $rv, $msgs ) = ftp_cmd( "quot", "site", "chmod", "644", $file );
        msg( "log", "Can't chmod $file on target:\n$msgs" )
          if $msgs;
        goto err if !$rv;
      } ## end foreach $file (@files)
    } else {
      for my $file (@files) {
        unless (chmod 0644, "$main::current_targetdir/$file") {
          $msgs = "Could not chmod $file: $!";
          goto err;
        }
      }
    }
  } ## end if ($conf::chmod_on_target)

  $main::dstat = "c";
  write_status_file() if $conf::statusdelay;
  return 1;

err:
  msg( "log,mail",
       "Upload to $conf::target failed",
       $? ? ", last exit status " . sprintf( "%s", $? >> 8 ) : "", "\n" );
  msg( "log,mail", "Error messages:\n", $msgs )
    if $msgs;

  # If "permission denied" was among the errors, test if the incoming is
  # writable at all.
  if ( $msgs && $msgs =~ /(permission denied|read-?only file)/i ) {
    if ( !check_incoming_writable() ) {
      msg( "log,mail", "(The incoming directory seems to be ",
           "unwritable.)\n" );
    }
  } ## end if ( $msgs =~ /(permission denied|read-?only file)/i)

  # remove bad files or an incomplete upload on target
  if ( $conf::upload_method eq "ssh" ) {
    ssh_cmd("rm -f @files");
  } elsif ( $conf::upload_method eq "ftp" ) {
    my $file;
    foreach $file (@files) {
      my ( $rv, $err );
      ( $rv, $err ) = ftp_cmd( "delete", $file );
      msg( "log", "Can't delete $file on target:\n$err" )
        if $err;
    } ## end foreach $file (@files)
  } else {
    my @tfiles = map { "$main::current_targetdir/$_" } @files;
    debug("executing unlink(@tfiles)");
    rm(@tfiles);
  }
  $main::dstat = "c";
  write_status_file() if $conf::statusdelay;
  return 0;
} ## end sub copy_to_target(@)

#
# check if a file is correctly signed with PGP
#
sub pgp_check($) {
  my $file   = shift;
  my $output = "";
  my $signator;
  my $found = 0;
  my $stat = 1;
  local (*PIPE);
  local $_;

  if ($file =~ /$re_file_safe/) {
    $file = $1;
  } else {
    msg( "log", "Tainted filename, skipping: $file\n" );
    return "LOCAL ERROR";
  }

  # check the file has only one clear-signed section
  my $fh;
  unless (open $fh, "<", $file) {
	  msg("log,mail", "Could not open $file\n");
	  return "";
  }
  unless (<$fh> eq "-----BEGIN PGP SIGNED MESSAGE-----\n") {
	  msg("log,mail", "$file: does not start with a clearsigned message\n");
	  return "";
  }
  my $pgplines = 1;
  while (<$fh>) {
	  if (/\A- /) {
		  msg("log,mail", "$file: dash-escaped messages are not accepted\n");
		  return "";
	  }
	  elsif ($_ eq "-----BEGIN PGP SIGNATURE-----\n"
		     || $_ eq "-----END PGP SIGNATURE-----\n") {
		  $pgplines++;
	  }
	  elsif (/\A--/) {
		  msg("log,mail", "$file: unexpected OpenPGP armor\n");
		  return "";
	  }
	  elsif ($pgplines > 3 && /\S/) {
		  msg("log,mail", "$file: found text after end of signature\n");
		  return "";
	  }
  }
  if ($pgplines != 3) {
	  msg("log,mail", "$file: doesn't seem to be a valid clearsigned OpenPGP message\n");
	  return "";
  }
  close $fh;

  if ( -x $conf::gpg ) {
    my @command = ("$conf::gpg", "--no-options", "--batch", "--no-tty",
                   "--trust-model", "always", "--no-default-keyring",
		   (map +("--keyring" => $_), @conf::keyrings),
		   "--verify", "-");
    debug(   "executing " . join(" ", @command) );

    my $child = open(PIPE, "-|");
    if (!defined($child)) {
      msg("log", "Can't open pipe to $conf::gpg: $!\n");
      return "LOCAL ERROR";
    }
    if ($child == 0) {
      unless (open(STDERR, ">&", \*STDOUT)) {
        print "Could not redirect STDERR.";
	exit(-1);
      }
      unless (open(STDIN, "<", $file)) {
        print "Could not open $file: $!";
	exit(-1);
      }
      { exec(@command) }; # BLOCK avoids warning about likely unreachable code
      print "Could not exec gpg: $!";
      exit(-1);
    }

    $output .= $_ while (<PIPE>);
    close(PIPE);
    $stat = $?;
  } ## end if ( -x $conf::gpg )

  if ($stat) {
    msg( "log,mail", "GnuPG signature check failed on $file\n" );
    msg( "mail",     $output );
    msg( "log,mail", "(Exit status ", $stat >> 8, ")\n" );
    return "";
  } ## end if ($stat)

  $output =~ /^(gpg: )?good signature from (user )?"(.*)"\.?$/im;
  ( $signator = $3 ) ||= "unknown signator";
  if ($conf::debug) {
    debug("GnuPG signature ok (by $signator)");
  }
  return $signator;
} ## end sub pgp_check($)

# ---------------------------------------------------------------------------
#							  the status daemon
# ---------------------------------------------------------------------------

#
# fork a subprocess that watches the 'status' FIFO
#
# that process blocks until someone opens the FIFO, then sends a
# signal (SIGUSR1) to the main process, expects
#
sub fork_statusd() {
  my $statusd_pid;
  my $main_pid = $$;
  my $errs;
  local (*STATFIFO);

  $statusd_pid = open( STATUSD, "|-" );
  die "cannot fork: $!\n" if !defined($statusd_pid);

  # parent just returns
  if ($statusd_pid) {
    msg( "log", "forked status daemon (pid $statusd_pid)\n" );
    return $statusd_pid;
  }

  # child: the status FIFO daemon

  # ignore SIGPIPE here, in case some closes the FIFO without completely
  # reading it
  $SIG{"PIPE"} = "IGNORE";

  # also ignore SIGCLD, we don't want to inherit the restart-statusd handler
  # from our parent
  $SIG{"CHLD"} = "DEFAULT";

  rm($conf::statusfile);
  POSIX::mkfifo($conf::statusfile, 0644)
    or die "Cannot create named pipe $conf::statusfile: $!\n";
  chmod( 0644, $conf::statusfile )
    or die "Cannot set modes of $conf::statusfile: $!\n";

  # close log file, so that log rotating works
  close(LOG);
  close(STDOUT);
  close(STDERR);

  while (1) {
    my ( $status, $mup, $incw, $ds, $next_run, $last_ping, $currch, $l );

    # open the FIFO for writing; this blocks until someone (probably ftpd)
    # opens it for reading
    open( STATFIFO, ">", $conf::statusfile )
      or die "Cannot open $conf::statusfile\n";
    select(STATFIFO);

    # tell main daemon to send us status infos
    kill( $main::signo{"USR1"}, $main_pid );

    # get the infos from stdin; must loop until enough bytes received!
    my $expect_len = 3 + 2 * STATNUM_LEN + STATSTR_LEN;
    for ( $status = "" ; ( $l = length($status) ) < $expect_len ; ) {
      sysread( STDIN, $status, $expect_len - $l, $l );
    }

    # disassemble the status byte stream
    my $pos = 0;
    foreach (
              [ mup       => 1 ],
              [ incw      => 1 ],
              [ ds        => 1 ],
              [ next_run  => STATNUM_LEN ],
              [ last_ping => STATNUM_LEN ],
              [ currch    => STATSTR_LEN ]
            )
    {
      eval "\$$_->[0] = substr( \$status, $pos, $_->[1] );";
      $pos += $_->[1];
    } ## end foreach ( [ mup => 1 ], [ incw...
    $currch =~ s/\n+//g;

    print_status( $mup, $incw, $ds, $next_run, $last_ping, $currch );
    close(STATFIFO);

    # This sleep is necessary so that we can't reopen the FIFO
    # immediately, in case the reader hasn't closed it yet if we get to
    # the open again. Is there a better solution for this??
    sleep 1;
  } ## end while (1)
} ## end sub fork_statusd()

#
# update the status file, in case we use a plain file and not a FIFO
#
sub write_status_file() {

  return if !$conf::statusfile;

  open( STATFILE, ">", $conf::statusfile )
    or ( msg( "log", "Could not open $conf::statusfile: $!\n" ), return );
  my $oldsel = select(STATFILE);

  print_status(
                $main::target_up,      $main::incoming_writable,
                $main::dstat,          $main::next_run,
                $main::last_ping_time, $main::current_changes
              );

  select($oldsel);
  close(STATFILE);
} ## end sub write_status_file()

sub print_status($$$$$$) {
  my $mup       = shift;
  my $incw      = shift;
  my $ds        = shift;
  my $next_run  = shift;
  my $last_ping = shift;
  my $currch    = shift;
  my $approx;
  my $version;

  ( $version = 'Release: 0.9 $Revision: 1.51 $' ) =~ s/\$ ?//g;
  print "debianqueued $version\n";

  $approx = $conf::statusdelay ? "approx. " : "";

  if ( $mup eq "0" ) {
    print "$conf::target is down, queue pausing\n";
    return;
  } elsif ( $conf::upload_method ne "copy" ) {
    print "$conf::target seems to be up, last ping $approx",
      print_time( time - $last_ping ), " ago\n";
  }

  if ( $incw eq "0" ) {
    print "The incoming directory is not writable, queue pausing\n";
    return;
  }

  if ( $ds eq "i" ) {
    print "Next queue check in $approx", print_time( $next_run - time ), "\n";
    return;
  } elsif ( $ds eq "c" ) {
    print "Checking queue directory\n";
  } elsif ( $ds eq "u" ) {
    print "Uploading to $conf::target\n";
  } else {
    print "Bad status data from daemon: \"$mup$incw$ds\"\n";
    return;
  }

  print "Current job is $currch\n" if $currch;
} ## end sub print_status($$$$$$)

#
# format a number for sending to statusd (fixed length STATNUM_LEN)
#
sub format_status_num(\$$) {
  my $varref = shift;
  my $num    = shift;

  $$varref = sprintf "%" . STATNUM_LEN . "d", $num;
} ## end sub format_status_num(\$$)

#
# format a string for sending to statusd (fixed length STATSTR_LEN)
#
sub format_status_str(\$$) {
  my $varref = shift;
  my $str    = shift;

  $$varref = substr( $str, 0, STATSTR_LEN );
  $$varref .= "\n" x ( STATSTR_LEN - length($$varref) );
} ## end sub format_status_str(\$$)

#
# send a status string to the status daemon
#
# Avoid all operations that could call malloc() here! Most libc
# implementations aren't reentrant, so we may not call it from a
# signal handler. So use only already-defined variables.
#
sub send_status() {
  local $! = 0;    # preserve errno

  # re-setup handler, in case we have broken SysV signals
  $SIG{"USR1"} = \&send_status;

  syswrite( STATUSD, $main::target_up,         1 );
  syswrite( STATUSD, $main::incoming_writable, 1 );
  syswrite( STATUSD, $main::dstat,             1 );
  syswrite( STATUSD, $main::next_run,          STATNUM_LEN );
  syswrite( STATUSD, $main::last_ping_time,    STATNUM_LEN );
  syswrite( STATUSD, $main::current_changes,   STATSTR_LEN );
} ## end sub send_status()

# ---------------------------------------------------------------------------
#							    FTP functions
# ---------------------------------------------------------------------------

#
# open FTP connection to target host if not already open
#
sub ftp_open() {
  return 1 unless $conf::upload_method eq "ftp";

  if ($main::FTP_chan) {

    # is already open, but might have timed out; test with a cwd
    return $main::FTP_chan
      if $main::FTP_chan->cwd($main::current_targetdir);

    # cwd didn't work, channel is closed, try to reopen it
    $main::FTP_chan = undef;
  } ## end if ($main::FTP_chan)

  if (
       !(
          $main::FTP_chan =
          Net::FTP->new(
                         $conf::target,
                         Debug   => $conf::ftpdebug,
                         Timeout => $conf::ftptimeout,
                         Passive => 1,
                       )
        )
     )
  {
    msg( "log,mail", "Cannot open FTP server $conf::target\n" );
    goto err;
  } ## end if ( !( $main::FTP_chan...
  if ( !$main::FTP_chan->login() ) {
    msg( "log,mail", "Anonymous login on FTP server $conf::target failed\n" );
    goto err;
  }
  if ( !$main::FTP_chan->binary() ) {
    msg( "log,mail", "Can't set binary FTP mode on $conf::target\n" );
    goto err;
  }
  if ( !$main::FTP_chan->cwd($main::current_targetdir) ) {
    msg( "log,mail",
         "Can't cd to $main::current_targetdir on $conf::target\n" );
    goto err;
  }
  debug("opened FTP channel to $conf::target");
  return 1;

err:
  $main::FTP_chan = undef;
  return 0;
} ## end sub ftp_open()

sub ftp_cmd($@) {
  my $cmd = shift;
  my ( $rv, $err );
  my $direct_resp_cmd = ( $cmd eq "quot" );

  debug( "executing FTP::$cmd(" . join( ", ", @_ ) . ")" );
  $SIG{"ALRM"} = sub { die "timeout in FTP::$cmd\n" };
  alarm($conf::remote_timeout);
  eval { $rv = $main::FTP_chan->$cmd(@_); };
  alarm(0);
  $err = "";
  $rv = ( ftp_code() =~ /^2/ ) ? 1 : 0 if $direct_resp_cmd;
  if ($@) {
    $err = $@;
    undef $rv;
  } elsif ( !$rv ) {
    $err = ftp_response();
  }
  return ( $rv, $err );
} ## end sub ftp_cmd($@)

sub ftp_close() {
  if ($main::FTP_chan) {
    $main::FTP_chan->quit();
    $main::FTP_chan = undef;
  }
  return 1;
} ## end sub ftp_close()

sub ftp_response() {
  return join( '', @{ ${*$main::FTP_chan}{'net_cmd_resp'} } );
}

sub ftp_code() {
  return ${*$main::FTP_chan}{'net_cmd_code'};
}

sub ftp_error() {
  my $code = ftp_code();
  return ( $code =~ /^[45]/ ) ? 1 : 0;
}

# ---------------------------------------------------------------------------
#							  utility functions
# ---------------------------------------------------------------------------

sub ssh_cmd($) {
  my $cmd = shift;
  my ( $msg, $stat );

  my $ecmd = "$conf::ssh $conf::ssh_options $conf::target "
    . "-l $conf::targetlogin \'cd $main::current_targetdir; $cmd\'";
  debug("executing $ecmd");
  $SIG{"ALRM"} = sub { die "timeout in ssh command\n" };
  alarm($conf::remote_timeout);
  eval { $msg = `$ecmd 2>&1`; };
  alarm(0);
  if ($@) {
    $msg  = $@;
    $stat = 1;
  } else {
    $stat = $?;
  }
  return ( $msg, $stat );
} ## end sub ssh_cmd($)

sub scp_cmd(@) {
  my ( $msg, $stat );

  my $ecmd = "$conf::scp $conf::ssh_options @_ "
    . "$conf::targetlogin\@$conf::target:$main::current_targetdir";
  debug("executing $ecmd");
  $SIG{"ALRM"} = sub { die "timeout in scp\n" };
  alarm($conf::remote_timeout);
  eval { $msg = `$ecmd 2>&1`; };
  alarm(0);
  if ($@) {
    $msg  = $@;
    $stat = 1;
  } else {
    $stat = $?;
  }
  return ( $msg, $stat );
} ## end sub scp_cmd(@)

#
# check if target is alive (code stolen from Net::Ping.pm)
#
sub check_alive(;$) {
  my $timeout = shift;
  my ( $saddr, $ret, $target_ip );
  local (*PINGSOCK);

  if ( $conf::upload_method eq "copy" ) {
    format_status_num( $main::last_ping_time, time );
    $main::target_up = 1;
    return;
  }

  $timeout ||= 30;

  if ( !( $target_ip = ( gethostbyname($conf::target) )[4] ) ) {
    msg( "log", "Cannot get IP address of $conf::target\n" );
    $ret = 0;
    goto out;
  }
  $saddr = pack( 'S n a4 x8', AF_INET, $main::echo_port, $target_ip );
  $SIG{'ALRM'} = sub { die };
  alarm($timeout);

  $ret = $main::tcp_proto;    # avoid warnings about unused variable
  $ret = 0;
  eval <<'EOM' ;
    return unless socket( PINGSOCK, PF_INET, SOCK_STREAM, $main::tcp_proto );
    return unless connect( PINGSOCK, $saddr );
    $ret = 1;
EOM
  alarm(0);
  close(PINGSOCK);
  msg( "log", "pinging $conf::target: " . ( $ret ? "ok" : "down" ) . "\n" );
out:
  $main::target_up = $ret ? "1" : "0";
  format_status_num( $main::last_ping_time, time );
  write_status_file() if $conf::statusdelay;
} ## end sub check_alive(;$)

#
# check if incoming dir on target is writable
#
sub check_incoming_writable() {
  my $testfile = ".debianqueued-testfile";
  my ( $msg, $stat );

  if ( $conf::upload_method eq "ssh" ) {
    ( $msg, $stat ) =
      ssh_cmd( "rm -f $testfile; touch $testfile; " . "rm -f $testfile" );
  } elsif ( $conf::upload_method eq "ftp" ) {
    my $file = "junk-for-writable-test-" . format_time();
    $file =~ s/[ :.]/-/g;
    local (*F);
    open( F, ">", $file );
    close(F);
    my $rv;
    ( $rv, $msg ) = ftp_cmd( "put", $file );
    $stat = 0;
    $msg = "" if !defined $msg;
    unlink $file;
    ftp_cmd( "delete", $file );
  } elsif ( $conf::upload_method eq "copy" ) {
    unless(POSIX::access($main::current_targetdir, &POSIX::W_OK)) {
      $msg = "No write access: $!";
      $stat = 1;
    }
  }
  chomp($msg);
  debug("exit status: $stat, output was: $msg");

  if ( !$stat ) {

    # change incoming_writable only if ssh didn't return an error
    $main::incoming_writable =
      ( $msg =~ /(permission denied|read-?only file|cannot create)/i )
      ? "0"
      : "1";
  } else {
    debug("local error, keeping old status");
  }
  debug("incoming_writable = $main::incoming_writable");
  write_status_file() if $conf::statusdelay;
  return $main::incoming_writable;
} ## end sub check_incoming_writable()

#
# remove a list of files, log failing ones
#
sub rm(@) {
  my $done = 0;

  foreach (@_) {
    ( unlink $_ and ++$done )
      or $! == ENOENT
      or msg( "log", "Could not delete $_: $!\n" );
  }
  return $done;
} ## end sub rm(@)

#
# get md5 checksum of a file
#
sub md5sum($) {
  my $file = shift;
  my $md5 = Digest::MD5->new;

  open my $fh, "<", $file or return "";
  $md5->addfile($fh);
  close $fh;

  return $md5->hexdigest;
} ## end sub md5sum($)

#
# output a messages to several destinations
#
# first arg is a comma-separated list of destinations; valid are "log"
# and "mail"; rest is stuff to be printed, just as with print
#
sub msg($@) {
  my @dest = split( ',', shift );

  if ( grep /log/, @dest ) {
    my $now = format_time();
    print LOG "$now ", @_;
  }

  if ( grep /mail/, @dest ) {
    $main::mail_text .= join( '', @_ );
  }
} ## end sub msg($@)

#
# print a debug messages, if $debug is true
#
sub debug(@) {
  return if !$conf::debug;
  my $now = format_time();
  print LOG "$now DEBUG ", @_, "\n";
}

#
# intialize the "mail" destination of msg() (this clears text,
# address, subject, ...)
#
sub init_mail(;$) {
  my $file = shift;

  $main::mail_addr    = "";
  $main::mail_text    = "";
  %main::packages     = ();
  $main::mail_subject = $file ? "Processing of $file" : "";
} ## end sub init_mail(;$)

#
# finalize mail to be sent from msg(): check if something present, and
# then send out
#
sub finish_mail() {

  debug("No mail for $main::mail_addr")
    if $main::mail_addr && !$main::mail_text;
  return unless $main::mail_addr && $main::mail_text;

  if ( !send_mail( $main::mail_addr, $main::mail_subject, $main::mail_text ) )
  {

    # store this mail in memory so it isn't lost if executing sendmail
    # failed.
    push(
          @main::stored_mails,
          {
            addr    => $main::mail_addr,
            subject => $main::mail_subject,
            text    => $main::mail_text
          }
        );
  } ## end if ( !send_mail( $main::mail_addr...
  init_mail();

  # try to send out stored mails
  my $mailref;
  while ( $mailref = shift(@main::stored_mails) ) {
    if (
         !send_mail( $mailref->{'addr'}, $mailref->{'subject'},
                     $mailref->{'text'} )
       )
    {
      unshift( @main::stored_mails, $mailref );
      last;
    } ## end if ( !send_mail( $mailref...
  } ## end while ( $mailref = shift(...
} ## end sub finish_mail()

#
# send one mail
#
sub send_mail($$$) {
  my $addr    = shift;
  my $subject = shift;
  my $text    = shift;

  my $package =
    keys %main::packages ? join( ' ', keys %main::packages ) : "";

  use Email::Sender::Simple;

  if ($conf::overridemail) {
	$addr = $conf::overridemail;
  }

  my $date = sprintf "%s",
    strftime( "%a, %d %b %Y %T %z", ( localtime(time) ) );
  my $message = <<__MESSAGE__;
To: $addr
From: Debian FTP Masters <ftpmaster\@ftp-master.debian.org>
Subject: $subject
Date: $date
X-Debian: DAK
X-DAK: DAK
Precedence: bulk
Auto-Submitted: auto-generated
__MESSAGE__

  if ( length $package ) {
    $message .= "X-Debian-Package: $package\n";
  }

  $message .= "\n$text";
  $message .= "\nGreetings,\n\n\tYour Debian queue daemon (running on host $main::hostname)\n";

  return Email::Sender::Simple->try_to_send($message);
} ## end sub send_mail($$$)

#
# try to find a mail address for a name in the keyrings
#
sub try_to_get_mail_addr($$) {
  my $name    = shift;
  my $listref = shift;

  @$listref = ();
  open( F,
            "$conf::gpg --no-options --batch --no-default-keyring "
          . "--always-trust --keyring "
          . join( " --keyring ", @conf::keyrings )
          . " --list-keys |"
      ) or return "";
  while (<F>) {
    if ( /^pub / && / $name / ) {
      /<([^>]*)>/;
      push( @$listref, $1 );
    }
  } ## end while (<F>)
  close(F);

  return ( @$listref >= 1 ) ? $listref->[0] : "";
} ## end sub try_to_get_mail_addr($$)

#
# return current time as string
#
sub format_time() {
  my $t;

  # omit weekday and year for brevity
  ( $t = localtime ) =~ /^\w+\s(.*)\s\d+$/;
  return $1;
} ## end sub format_time()

sub print_time($) {
  my $secs = shift;
  my $hours = int( $secs / ( 60 * 60 ) );

  $secs -= $hours * 60 * 60;
  return sprintf "%d:%02d:%02d", $hours, int( $secs / 60 ), $secs % 60;
} ## end sub print_time($)

#
# block some signals during queue processing
#
# This is just to avoid data inconsistency or uploads being aborted in the
# middle. Only "soft" signals are blocked, i.e. SIGINT and SIGTERM, try harder
# ones if you really want to kill the daemon at once.
#
sub block_signals() {
  POSIX::sigprocmask( SIG_BLOCK, $main::block_sigset );
}

sub unblock_signals() {
  POSIX::sigprocmask( SIG_UNBLOCK, $main::block_sigset );
}

#
# process SIGHUP: close log file and reopen it (for logfile cycling)
#
sub close_log($) {
  close(LOG);
  close(STDOUT);
  close(STDERR);

  open( LOG, ">>", $conf::logfile )
    or die "Cannot open my logfile $conf::logfile: $!\n";
  chmod( 0644, $conf::logfile )
    or msg( "log", "Cannot set modes of $conf::logfile: $!\n" );
  select( ( select(LOG), $| = 1 )[0] );

  open( STDOUT, ">&", \*LOG )
    or msg( "log",
      "$main::progname: Can't redirect stdout to " . "$conf::logfile: $!\n" );
  open( STDERR, ">&", \*LOG )
    or msg( "log",
      "$main::progname: Can't redirect stderr to " . "$conf::logfile: $!\n" );
  msg( "log", "Restart after SIGHUP\n" );
} ## end sub close_log($)

#
# process SIGCHLD: check if it was our statusd process
#
sub kid_died($) {
  my $pid;

  # reap statusd, so that it's no zombie when we try to kill(0) it
  waitpid( $main::statusd_pid, WNOHANG );

  # Uncomment the following line if your Perl uses unreliable System V signal
  # (i.e. if handlers reset to default if the signal is delivered).
  # (Unfortunately, the re-setup can't be done in any case, since on some
  # systems this will cause the SIGCHLD to be delivered again if there are
  # still unreaped children :-(( )

  #	 $SIG{"CHLD"} = \&kid_died; # resetup handler for SysV
} ## end sub kid_died($)

sub restart_statusd() {

  # restart statusd if it died
  if ( !kill( 0, $main::statusd_pid ) ) {
    close(STATUSD);    # close out pipe end
    $main::statusd_pid = fork_statusd();
  }
} ## end sub restart_statusd()

#
# process a fatal signal: cleanup and exit
#
sub fatal_signal($) {
  my $signame = shift;
  my $sig;

  # avoid recursions of fatal_signal in case of BSD signals
  foreach $sig (qw( ILL ABRT BUS FPE SEGV PIPE )) {
    $SIG{$sig} = "DEFAULT";
  }

  if ( $$ == $main::maind_pid ) {

    # only the main daemon should do this
    kill( $main::signo{"TERM"}, $main::statusd_pid )
      if defined $main::statusd_pid;
    unlink( $conf::statusfile, $conf::pidfile );
  } ## end if ( $$ == $main::maind_pid)
  msg( "log", "Caught SIG$signame -- exiting (pid $$)\n" );
  exit 1;
} ## end sub fatal_signal($)

# Local Variables:
#  tab-width: 4
#  fill-column: 78
# End:
