#!/usr/bin/perl
# $Id: service.in,v 1.118 2005/10/19 01:22:14 don Exp $
#
# Usage: service <code>.nn
# Temps:  incoming/P<code>.nn

use warnings;
use strict;

use POSIX qw(strftime locale_h);
setlocale(LC_TIME, "C");

use Debbugs::Config qw(:globals :config);

use File::Copy;
use MIME::Parser;

use Params::Validate qw(:types validate_with);

use Debbugs::Common qw(:util :quit :misc :lock);

use Debbugs::Status qw(:read :status :write :versions :hook);
use Debbugs::Packages qw(binary_to_source);

use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522 create_mime_message);
use Debbugs::Mail qw(send_mail_message);
use Debbugs::User;
use Debbugs::Recipients qw(:all);
use HTML::Entities qw(encode_entities);
use Debbugs::Versions::Dpkg;

use Debbugs::Status qw(splitpackages);

use Debbugs::CGI qw(html_escape);
use Debbugs::Control qw(:all);
use Debbugs::Control::Service qw(:all);
use Debbugs::Log qw(:misc);
use Debbugs::Text qw(:templates);

use Scalar::Util qw(looks_like_number);

use List::AllUtils qw(first);

use Mail::RFC822::Address;
use Encode qw(decode encode);

chdir($config{spool_dir}) or
     die "Unable to chdir to spool_dir '$config{spool_dir}': $!";

my $debug = 0;
umask(002);

my ($nn,$control) = $ARGV[0] =~ m/^(([RC])\.\d+)$/;
if (not defined $control or not defined $nn) {
     die "Bad argument to service.in";
}
if (!rename("incoming/G$nn","incoming/P$nn")) {
    defined $! and $! =~ m/no such file or directory/i and exit 0;
    die "Failed to rename incoming/G$nn to incoming/P$nn: $!";
}

my $log_fh = IO::File->new("incoming/P$nn",'r') or
     die "Unable to open incoming/P$nn for reading: $!";
my @log=<$log_fh>;
my @msg=@log;
close($log_fh);

chomp @msg;

print "###\n",join("##\n",@msg),"\n###\n" if $debug;

# Bug numbers to send e-mail to, hash so that we don't send to the
# same bug twice.
my (%bug_affected);

my (@headerlines,@bodylines);

my $parse_output = Debbugs::MIME::parse(join('',@log));
@headerlines = @{$parse_output->{header}};
@bodylines = @{$parse_output->{body}};

my %header;
for (@headerlines) {
    $_ = decode_rfc1522($_);
    s/\n\s/ /g;
    print ">$_<\n" if $debug;
    if (s/^(\S+):\s*//) {
	my $v = lc $1;
	print ">$v=$_<\n" if $debug;
	$header{$v} = $_;
    } else {
	print "!>$_<\n" if $debug;
    }
}
$header{'message-id'} ||= '';
$header{subject} ||= '';

grep(s/\s+$//,@bodylines);

print "***\n",join("\n",@bodylines),"\n***\n" if $debug;

if (defined $header{'resent-from'} && !defined $header{'from'}) {
    $header{'from'} = $header{'resent-from'};
}

defined($header{'from'}) || die "no From header";

delete $header{'reply-to'} 
	if ( defined($header{'reply-to'}) && $header{'reply-to'} =~ m/^\s*$/ );

my $replyto;
if ( defined($header{'reply-to'}) && $header{'reply-to'} ne "" ) {
    $replyto = $header{'reply-to'};
} else {
    $replyto = $header{'from'};
}

# This is an error counter which should be incremented every time there is an error.
my $errors = 0;
my $controlrequestaddr= ($control ? 'control' : 'request').'@'.$config{email_domain};
my $transcript_scalar = '';
open my $transcript, ">:scalar:utf8", \$transcript_scalar or
     die "Unable to create transcript scalar: $!";
print {$transcript} "Processing commands for $controlrequestaddr:\n\n";


my $dl = 0;
my %affected_packages;
my %recipients;
# this is the hashref which is passed to all control calls
my %limit = ();


my @common_control_options =
    (transcript        => $transcript,
     requester         => $header{from},
     request_addr      => $controlrequestaddr,
     request_msgid     => $header{'message-id'},
     request_subject   => $header{subject},
     request_nn        => $nn,
     request_replyto   => $replyto,
     message           => \@log,
     affected_bugs     => \%bug_affected,
     affected_packages => \%affected_packages,
     recipients        => \%recipients,
     limit             => \%limit,
    );

my $state= 'idle';
my $lowstate= 'idle';
my $mergelowstate= 'idle';
my $midix=0;

my $user = $replyto;
$user =~ s/,.*//;
$user =~ s/^.*<(.*)>.*$/$1/;
$user =~ s/[(].*[)]//;
$user =~ s/^\s*(\S+)\s+.*$/$1/;
$user = "" unless (Debbugs::User::is_valid_user($user));
my $indicated_user = 0;

my $quickabort = 0;


if (@gExcludeFromControl and grep {$replyto =~ m/\Q$_\E/} @gExcludeFromControl) {
	print {$transcript} fill_template('mail/excluded_from_control');
	$quickabort = 1;
}

my %limit_pkgs = ();
my %clonebugs = ();
my %bcc = ();


our $data;
our $message;
our $extramessage;
our $ref;

our $mismatch;
our $action;


my $ok = 0;
my $unknowns = 0;
my $procline=0;
for ($procline=0; $procline<=$#bodylines; $procline++) {
    my $noriginator;
    my $newsubmitter;
    my $oldsubmitter;
    my $newowner;
    $state eq 'idle' || print "state: $state ?\n";
    $lowstate eq 'idle' || print "lowstate: $lowstate ?\n";
    $mergelowstate eq 'idle' || print "mergelowstate: $mergelowstate ?\n";
    if ($quickabort) {
         print {$transcript} "Stopping processing here.\n\n";
	 last;
    }
    $_= $bodylines[$procline]; s/\s+$//;
    # Remove BOM markers from UTF-8 strings
    # Fixes #488554
    s/\xef\xbb\xbf//g;
    next unless m/\S/;
    eval {
	my $temp = decode("utf8",$_,Encode::FB_CROAK);
	$_ = $temp;
    };
    print {$transcript} "> $_\n";
    next if m/^\s*\#/;
    $action= '';
    if (m/^(?:stop|quit|--|thank(?:s|\s*you)?|kthxbye)\.*\s*$/i) {
	print {$transcript} "Stopping processing here.\n\n";
        last;
    } elsif (m/^debug\s+(\d+)$/i && $1 >= 0 && $1 <= 1000) {
        $dl= $1+0;
	if ($dl > 0 and not grep /debug/,@common_control_options) {
	    push @common_control_options,(debug => $transcript);
	}
	print {$transcript} "Debug level $dl.\n\n";
    } elsif (m/^(send|get)\s+\#?(\d{2,})$/i) {
        $ref= $2+0;
        &sendlynxdoc("bugreport.cgi?bug=$ref","logs for $gBug#$ref");
    } elsif (m/^send-detail\s+\#?(\d{2,})$/i) {
	$ref= $1+0;
	&sendlynxdoc("bugreport.cgi?bug=$ref&boring=yes",
		     "detailed logs for $gBug#$ref");
    } elsif (m/^index(\s+full)?$/i) {
	print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
	$errors++;
	$ok++; # well, it's not really ok, but it fixes #81224 :)
    } elsif (m/^index-summary\s+by-package$/i) {
	print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
	$errors++;
	$ok++; # well, it's not really ok, but it fixes #81224 :)
    } elsif (m/^index-summary(\s+by-number)?$/i) {
	print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
	$errors++;
	$ok++; # well, it's not really ok, but it fixes #81224 :)
    } elsif (m/^index(\s+|-)pack(age)?s?$/i) {
	&sendlynxdoc("pkgindex.cgi?indexon=pkg",'index of packages');
    } elsif (m/^index(\s+|-)maints?$/i) {
	&sendlynxdoc("pkgindex.cgi?indexon=maint",'index of maintainers');
    } elsif (m/^index(\s+|-)maint\s+(\S+)$/i) {
	my $maint = $2;
	&sendlynxdoc("pkgreport.cgi?maint=" . urlsanit($maint),
		     "$gBug list for maintainer \`$maint'");
        $ok++;
    } elsif (m/^index(\s+|-)pack(age)?s?\s+(\S.*\S)$/i) {
	my $package = $+;
	&sendlynxdoc("pkgreport.cgi?pkg=" . urlsanit($package),
		     "$gBug list for package $package");
        $ok++;
    } elsif (m/^send-unmatched(\s+this|\s+-?0)?$/i) {
	print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
	$errors++;
	$ok++; # well, it's not really ok, but it fixes #81224 :)
    } elsif (m/^send-unmatched\s+(last|-1)$/i) {
	print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
	$errors++;
	$ok++; # well, it's not really ok, but it fixes #81224 :)
    } elsif (m/^send-unmatched\s+(old|-2)$/i) {
	print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
	$errors++;
	$ok++; # well, it's not really ok, but it fixes #81224 :)
    } elsif (m/^getinfo\s+([\w.-]+)$/i) {
        # the following is basically a Debian-specific kludge, but who cares
        my $req = $1;
	if ($req =~ /^maintainers$/i && -f "$gConfigDir/Maintainers") {
	    &sendinfo("local", "$gConfigDir/Maintainers", "Maintainers file");
	} elsif ($req =~ /^override\.(\w+)\.([\w.-]+)$/i) {
	    $req =~ s/.gz$//;
	    &sendinfo("ftp.d.o", "$req", "override file for $2 part of $1 distribution");
	} elsif ($req =~ /^pseudo-packages\.(description|maintainers)$/i && -f "$gConfigDir/$req") {
	    &sendinfo("local", "$gConfigDir/$req", "$req file");
	} else {
	    print {$transcript} "Info file $req does not exist.\n\n";
	}
    } elsif (m/^help/i) {
        &sendhelp;
        print {$transcript} "\n";
        $ok++;
    } elsif (m/^refcard/i) {
        &sendtxthelp("bug-mailserver-refcard.txt","mail servers' reference card");
    } elsif (m/^subscribe/i) {
        print {$transcript} <<END;
There is no $gProject $gBug mailing list.  If you wish to review bug reports
please do so via $gWebDomain or ask this mail server
to send them to you.
soon: MAILINGLISTS_TEXT
END
    } elsif (m/^unsubscribe/i) {
        print {$transcript} <<END;
soon: UNSUBSCRIBE_TEXT
soon: MAILINGLISTS_TEXT
END
    } elsif (m/^user\s+(\S+)\s*$/i) {
        my $newuser = $1;
	if (Debbugs::User::is_valid_user($newuser)) {
	    my $olduser = ($user ne "" ? " (was $user)" : "");
            print {$transcript} "Setting user to $newuser$olduser.\n";
	    $user = $newuser;
	    $indicated_user = 1;
	} else {
	    print {$transcript} "Selected user id ($newuser) invalid, sorry\n";
	    $errors++;
	    $user = "";
	    $indicated_user = 1;
	}
    } elsif (m/^usercategory\s+(\S+)(\s+\[hidden\])?\s*$/i) {
        $ok++;
	my $catname = $1;
	my $hidden = (defined $2 and $2 ne "");

        my $prefix = "";
        my @cats;
        my $bad = 0;
	my $catsec = 0;
	if ($user eq "") {
	    print {$transcript} "No valid user selected\n";
	    $errors++;
	    next;
        }
	if (not $indicated_user and defined $user) {
	     print {$transcript} "User is $user\n";
	     $indicated_user = 1;
	}
	my @ords = ();
	while (++$procline <= $#bodylines) {
            unless ($bodylines[$procline] =~ m/^\s*([*+])\s*(\S.*)$/) {
                $procline--;
                last;
            }
            print {$transcript} "> $bodylines[$procline]\n";
            next if $bad;
            my ($o, $txt) = ($1, $2);
            if ($#cats == -1 && $o eq "+") {
                print {$transcript} "User defined category specification must start with a category name. Skipping.\n\n";
		$errors++;
                $bad = 1;
                next;
            }
            if ($o eq "+") {
	        unless (ref($cats[-1]) eq "HASH") {
		    $cats[-1] = { "nam" => $cats[-1], 
		                  "pri" => [], "ttl" => [] };
		}
	        $catsec++;
		my ($desc, $ord, $op);
                if ($txt =~ m/^(.*\S)\s*\[((\d+):\s*)?\]\s*$/) {
                    $desc = $1; $ord = $3; $op = "";
                } elsif ($txt =~ m/^(.*\S)\s*\[((\d+):\s*)?(\S+)\]\s*$/) {
                    $desc = $1; $ord = $3; $op = $4;
                } elsif ($txt =~ m/^([^[\s]+)\s*$/) {
                    $desc = ""; $op = $1;
                } else {
                    print {$transcript} "Unrecognised syntax for category section. Skipping.\n\n";
		    $errors++;
                    $bad = 1;
                    next;
                }
		$ord = 999 unless defined $ord;

		if ($op) {
                    push @{$cats[-1]->{"pri"}}, $prefix . $op;
		    push @{$cats[-1]->{"ttl"}}, $desc;
		    push @ords, "$ord $catsec";
		} else {
		    $cats[-1]->{"def"} = $desc;
		    push @ords, "$ord DEF";
		    $catsec--;
		}
		@ords = sort {
		    my ($a1, $a2, $b1, $b2) = split / /, "$a $b";
		    ((looks_like_number($a1) and looks_like_number($a2))?$a1 <=> $b1:$a1 cmp $b1) ||
		    ((looks_like_number($a2) and looks_like_number($b2))?$a2 <=> $b2:$a2 cmp $b2);
		} @ords;
		$cats[-1]->{"ord"} = [map { m/^.* (\S+)/; $1 eq "DEF" ? $catsec + 1 : $1 } @ords];
            } elsif ($o eq "*") {
	        $catsec = 0;
                my ($name);
                if ($txt =~ m/^(.*\S)(\s*\[(\S+)\])\s*$/) {
                    $name = $1; $prefix = $3;
                } else {
                    $name = $txt; $prefix = "";
                }
                push @cats, $name;
            }
        }
        # XXX: got @cats, now do something with it
	my $u = Debbugs::User::get_user($user);
	if (@cats) {
	    print {$transcript} "Added usercategory $catname.\n\n";
	    $u->{"categories"}->{$catname} = [ @cats ];
	    if (not $hidden) {
		 push @{$u->{visible_cats}},$catname;
	    }
	} else {
	    print {$transcript} "Removed usercategory $catname.\n\n";
	    delete $u->{"categories"}->{$catname};
	    @{$u->{visible_cats}} = grep {$_ ne $catname} @{$u->{visible_cats}};
	}
	$u->write();
    } elsif (m/^usertags?\s+\#?(-?\d+)\s+(([=+-])\s*)?(\S.*)?$/i) {
	$ok++;
	$ref = $1;
	my $addsubcode = $3 || "+";
	my $tags = $4;
	if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) {
	     $ref = $clonebugs{$ref};
        }
	if ($user eq "") {
	    print {$transcript} "No valid user selected\n";
	    $errors++;
	    $indicated_user = 1;
        } elsif (check_limit(data => read_bug(bug => $ref),
			     limit => \%limit,
			     transcript => $transcript)) {
	    if (not $indicated_user and defined $user) {
		 print {$transcript} "User is $user\n";
		 $indicated_user = 1;
	    }
	    my %ut;
	    Debbugs::User::read_usertags(\%ut, $user);
            my @oldtags = (); my @newtags = (); my @badtags = ();
	    my %chtags;
	    if (defined $tags and length $tags) {
		 for my $t (split /[,\s]+/, $tags) {
		      if ($t =~ m/^[a-zA-Z0-9.+\@-]+$/) {
			   $chtags{$t} = 1;
		      } else {
			   push @badtags, $t;
		      }
		 }
	    }
	    if (@badtags) {
                print {$transcript} "Ignoring illegal tag/s: ".join(', ', @badtags).".\nPlease use only alphanumerics, at, dot, plus and dash.\n";
		$errors++;
	    }
            for my $t (keys %chtags) {
	        $ut{$t} = [] unless defined $ut{$t};
	    }
	    for my $t (keys %ut) {
		my %res = map { ($_, 1) } @{$ut{$t}};
		push @oldtags, $t if defined $res{$ref};
		my $addop = ($addsubcode eq "+" or $addsubcode eq "=");
		my $del = (defined $chtags{$t} ? $addsubcode eq "-" 
		                               : $addsubcode eq "=");
		$res{$ref} = 1 if ($addop && defined $chtags{$t});
		delete $res{$ref} if ($del);
		push @newtags, $t if defined $res{$ref};
		$ut{$t} = [ sort { $a <=> $b } (keys %res) ];
	    }
	    if (@oldtags == 0) {
		print {$transcript} "There were no usertags set.\n";
	    } else {
		print {$transcript} "Usertags were: " . join(" ", @oldtags) . ".\n";
	    }
	    print {$transcript} "Usertags are now: " . join(" ", @newtags) . ".\n";
	    Debbugs::User::write_usertags(\%ut, $user);
	}
    } elsif (!$control) {
        print {$transcript} <<END;
Unknown command or malformed arguments to command.
(Use control\@$gEmailDomain to manipulate reports.)

END
	#### "developer only" ones start here
    } elsif (defined valid_control($_)) {
	my ($new_errors,$terminate_control) =
	    control_line(line => $_,
			 clonebugs => \%clonebugs,
			 limit => \%limit,
			 common_control_options => \@common_control_options,
			 errors => \$errors,
			 transcript => $transcript,
			 debug => $debug,
			 ok => \$ok,
			 replyto => $replyto,
			);
	if ($terminate_control) {
	    last;
	}
    } else {
	print {$transcript} "Unknown command or malformed arguments to command.\n";
	$errors++;
        if (++$unknowns >= 5) {
            print {$transcript} "Too many unknown commands, stopping here.\n\n";
            last;
        }
    }
}
if ($procline>$#bodylines) {
    print {$transcript} ">\nEnd of message, stopping processing here.\n\n";
}
if (!$ok && !$quickabort) {
    $errors++;
    print {$transcript} "No commands successfully parsed; sending the help text(s).\n";
    &sendhelp;
    print {$transcript} "\n";
}

my @maintccs = determine_recipients(recipients => \%recipients,
				    address_only => 1,
				    cc => 1,
				   );
if (!defined $header{'subject'} || $header{'subject'} eq "") {
  $header{'subject'} = "your mail";
}

# Error text here advertises how many errors there were
my $error_text = $errors > 0 ? " (with $errors error" . ($errors > 1 ? "s" : "") . ")" : "";

my @common_headers;
push @common_headers, 'X-Loop',$gMaintainerEmail;

my $temp_transcript = $transcript_scalar;
eval{
    $temp_transcript = decode("utf8",$temp_transcript,Encode::FB_CROAK);
};
my $reply =
    create_mime_message([From          => qq("$gProject $gBug Tracking System" <$gMaintainerEmail>),
			 To            => $replyto,
			 @maintccs ? (Cc => join(', ',@maintccs)):(),
			 Subject       => "Processed${error_text}: $header{subject}",
			 'Message-ID'  => "<handler.s.$nn.transcript\@$gEmailDomain>",
			 'In-Reply-To' => $header{'message-id'},
			 References    => join(' ',grep {defined $_} $header{'message-id'},$data->{msgid}),
			 Precedence    => 'bulk',
			 keys %affected_packages ?("X-${gProject}-PR-Package" => join(' ',keys %affected_packages)):(),
			 keys %affected_packages ?("X-${gProject}-PR-Source" =>
						   join(' ',
							map {defined $_ ?(ref($_)?@{$_}:$_):()}
							binary_to_source(binary => [keys %affected_packages],
									 source_only => 1))):(),
			 "X-$gProject-PR-Message" => 'transcript',
			 @common_headers,
			],
			fill_template('mail/message_body',
				      {body => "${temp_transcript}Please contact me if you need assistance."},
				     ));

my $repliedshow= join(', ',$replyto,
		      determine_recipients(recipients => \%recipients,
					   cc => 1,
					   address_only => 1,
					  )
		     );

utime(time,time,"db-h");

&sendmailmessage($reply,
		 exists $header{'x-debbugs-no-ack'}?():$replyto,
		 make_list(values %{{determine_recipients(recipients => \%recipients,
							  address_only => 1,
							 )}}
			  ),
		);

unlink("incoming/P$nn") || die "unlinking incoming/P$nn: $!";

sub sendmailmessage {
    my ($message,@recips) = @_;
    $message = "X-Loop: $gMaintainerEmail\n" . $message;
    send_mail_message(message    => $message,
		      recipients => \@recips,
		     );
    $midix++;
}

sub fill_template{
     my ($template,$extra_var) = @_;
     $extra_var ||={};
     my $variables = {config => \%config,
		      defined($ref)?(ref    => $ref):(),
		      defined($data)?(data  => $data):(),
		      refs => [map {exists $clonebugs{$_}?$clonebugs{$_}:$_} keys %bug_affected],
		      %{$extra_var},
		     };
     my $hole_var = {'&bugurl' =>
		     sub{"$_[0]: ".
			      $config{cgi_domain}.'/'.
				   Debbugs::CGI::bug_links(bug=>$_[0],
							   links_only => 1,
							  );
		    }
		    };
     return fill_in_template(template => $template,
			     variables => $variables,
			     hole_var  => $hole_var,
			    );
}

=head2 message_body_template

     message_body_template('mail/ack',{ref=>'foo'});

Creates a message body using a template

=cut

sub message_body_template{
     my ($template,$extra_var) = @_;
     $extra_var ||={};
     my $body = fill_template($template,$extra_var);
     return fill_template('mail/message_body',
			  {%{$extra_var},
			   body => $body,
			  },
			 );
}

sub sendhelp {
     if ($control) {
	  &sendtxthelpraw("bug-maint-mailcontrol.txt","instructions for control\@$gEmailDomain")
     }
     else {
	  &sendtxthelpraw("bug-log-mailserver.txt","instructions for request\@$gEmailDomain");
     }
}

#sub unimplemented {
#    print {$transcript} "Sorry, command $_[0] not yet implemented.\n\n";
#}
our %checkmatch_values;
sub checkmatch {
    my ($string,$mvarname,$svarvalue,@newmergelist) = @_;
    my ($mvarvalue);
    if (@newmergelist) {
	$mvarvalue = $checkmatch_values{$mvarname};
        print {$transcript} "D| checkmatch \`$string' /$mvarname/$mvarvalue/$svarvalue/\n"
            if $dl;
        $mismatch .=
            "Values for \`$string' don't match:\n".
            " #$newmergelist[0] has \`$mvarvalue';\n".
            " #$ref has \`$svarvalue'\n"
            if $mvarvalue ne $svarvalue;
    } else {
	 print {$transcript} "D| setupmatch \`$string' /$mvarname/$svarvalue/\n"
	      if $dl;
	 $checkmatch_values{$mvarname} = $svarvalue;
    }
}

sub checkpkglimit {
    if (keys %limit_pkgs and not defined $limit_pkgs{$data->{package}}) {
        print {$transcript} "$gBug number $ref belongs to package $data->{package}, skipping.\n\n";
        $errors++;
        return 0;
    }
    return 1;
}

sub manipset {
    my $list = shift;
    my $elt = shift;
    my $add = shift;

    my %h = map { $_ => 1 } split ' ', $list;
    if ($add) {
        $h{$elt}=1;
    }
    else {
	delete $h{$elt};
    }
    return join ' ', sort keys %h;
}

# High-level bug manipulation calls
# Do announcements themselves
#
# Possible calling sequences:
#    setbug (returns 0)
#    
#    setbug (returns 1)
#    &transcript(something)
#    nochangebug
#
#    setbug (returns 1)
#    $action= (something)
#    do {
#      (modify s_* variables)
#    } while (getnextbug);

our $manybugs;

sub nochangebug {
    &dlen("nochangebug");
    $state eq 'single' || $state eq 'multiple' || die "$state ?";
    &cancelbug;
    &endmerge if $manybugs;
    $state= 'idle';
    &dlex("nochangebug");
}

our $sref;
our @thisbugmergelist;

sub setbug {
    &dlen("setbug $ref");
    if ($ref =~ m/^-\d+/) {
        if (!defined $clonebugs{$ref}) {
            &notfoundbug;
            &dlex("setbug => noclone");
            return 0;
        }
        $ref = $clonebugs{$ref};
    }
    $state eq 'idle' || die "$state ?";
    if (!&getbug) {
        &notfoundbug;
        &dlex("setbug => 0s");
        return 0;
    }

    if (!&checkpkglimit) {
        &cancelbug;
        return 0;
    }

    @thisbugmergelist= split(/ /,$data->{mergedwith});
    if (!@thisbugmergelist) {
        &foundbug;
        $manybugs= 0;
        $state= 'single';
        $sref=$ref;
        &dlex("setbug => 1s");
        return 1;
    }
    &cancelbug;
    &getmerge;
    $manybugs= 1;
    if (!&getbug) {
        &notfoundbug;
        &endmerge;
        &dlex("setbug => 0mc");
        return 0;
    }
    &foundbug;
    $state= 'multiple'; $sref=$ref;
    &dlex("setbug => 1m");
    return 1;
}

sub getnextbug {
    &dlen("getnextbug");
    $state eq 'single' || $state eq 'multiple' || die "$state ?";
    &savebug;
    if (!$manybugs || !@thisbugmergelist) {
        length($action) || die;
        print {$transcript} "$action\n$extramessage\n";
        &endmerge if $manybugs;
        $state= 'idle';
        &dlex("getnextbug => 0");
        return 0;
    }
    $ref= shift(@thisbugmergelist);
    &getbug || die "bug $ref disappeared";
    &foundbug;
    &dlex("getnextbug => 1");
    return 1;
}

# Low-level bug-manipulation calls
# Do no announcements
#
#    getbug (returns 0)
#
#    getbug (returns 1)
#    cancelbug
#
#    getmerge
#    $action= (something)
#    getbug (returns 1)
#    savebug/cancelbug
#    getbug (returns 1)
#    savebug/cancelbug
#    [getbug (returns 0)]
#    &transcript("$action\n\n")
#    endmerge

sub notfoundbug { print {$transcript} "$gBug number $ref not found. (Is it archived?)\n\n"; }
sub foundbug { print {$transcript} "$gBug#$ref: $data->{subject}\n"; }

sub getmerge {
    &dlen("getmerge");
    $mergelowstate eq 'idle' || die "$mergelowstate ?";
    &filelock('lock/merge');
    $mergelowstate='locked';
    &dlex("getmerge");
}

sub endmerge {
    &dlen("endmerge");
    $mergelowstate eq 'locked' || die "$mergelowstate ?";
    &unfilelock;
    $mergelowstate='idle';
    &dlex("endmerge");
}

sub getbug {
    &dlen("getbug $ref");
    $lowstate eq 'idle' || die "$state ?";
    # Only use unmerged bugs here
    if (($data = &lockreadbug($ref,'db-h'))) {
        $sref= $ref;
        $lowstate= "open";
        &dlex("getbug => 1");
        $extramessage='';
        return 1;
    }
    $lowstate= 'idle';
    &dlex("getbug => 0");
    return 0;
}

sub cancelbug {
    &dlen("cancelbug");
    $lowstate eq 'open' || die "$state ?";
    &unfilelock;
    $lowstate= 'idle';
    &dlex("cancelbug");
}

sub savebug {
    &dlen("savebug $ref");
    $lowstate eq 'open' || die "$lowstate ?";
    length($action) || die;
    $ref == $sref || die "read $sref but saving $ref ?";
    append_action_to_log(bug => $ref,
			 action => $action,
			 requester => $header{from},
			 request_addr => $controlrequestaddr,
			 message => \@log,
			 get_lock => 0,
			);
    unlockwritebug($ref, $data);
    $lowstate= "idle";
    &dlex("savebug");
}

sub dlen {
    return if !$dl;
    print {$transcript} "C> @_ ($state $lowstate $mergelowstate)\n";
}

sub dlex {
    return if !$dl;
    print {$transcript} "R> @_ ($state $lowstate $mergelowstate)\n";
}

sub urlsanit {
    my $url = shift;
    $url =~ s/%/%25/g;
    $url =~ s/\+/%2b/g;
    my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
    $url =~ s/([<>&"])/\&$saniarray{$1};/g;
    return $url;
}

sub sendlynxdoc {
    &sendlynxdocraw;
    print {$transcript} "\n";
    $ok++;
}

sub sendtxthelp {
    &sendtxthelpraw;
    print {$transcript} "\n";
    $ok++;
}


our $doc;
sub sendtxthelpraw {
    my ($relpath,$description) = @_;
    $doc='';
    if (not -e "$gDocDir/$relpath") {
	print {$transcript} "Unfortunatly, the help text doesn't exist, so it wasn't sent.\n";
	warn "Help text $gDocDir/$relpath not found";
	return;
    }
    open(D,"$gDocDir/$relpath") || die "open doc file $relpath: $!";
    while(<D>) { $doc.=$_; }
    close(D);
    print {$transcript} "Sending $description in separate message.\n";
    &sendmailmessage(<<END.$doc,$replyto);
From: "$gProject $gBug Tracking System" <$gMaintainerEmail>
To: $replyto
Subject: $gProject $gBug help: $description
References: $header{'message-id'}
In-Reply-To: $header{'message-id'}
Message-ID: <handler.s.$nn.help.$midix\@$gEmailDomain>
Precedence: bulk
X-$gProject-PR-Message: doc-text $relpath

END
    $ok++;
}

sub sendlynxdocraw {
    my ($relpath,$description) = @_;
    $doc='';
    open(L,"lynx -nolist -dump $gCGIDomain/\Q$relpath\E 2>&1 |") || die "fork for lynx: $!";
    while(<L>) { $doc.=$_; }
    $!=0; close(L);
    if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
        print {$transcript} "Information ($description) is not available -\n".
	     "perhaps the $gBug does not exist or is not on the WWW yet.\n";
         $ok++;
    } elsif ($?) {
        print {$transcript} "Error getting $description (code $? $!):\n$doc\n";
    } else {
        print {$transcript} "Sending $description.\n";
        &sendmailmessage(<<END.$doc,$replyto);
From: "$gProject $gBug Tracking System" <$gMaintainerEmail>
To: $replyto
Subject: $gProject $gBugs information: $description
References: $header{'message-id'}
In-Reply-To: $header{'message-id'}
Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
Precedence: bulk
X-$gProject-PR-Message: doc-html $relpath

END
         $ok++;
    }
}


sub sendinfo {
    my ($wherefrom,$path,$description) = @_;
    if ($wherefrom eq "ftp.d.o") {
      $doc = `lynx -nolist -dump http://ftp.debian.org/debian/indices/$path.gz 2>&1 | gunzip -cf` or die "fork for lynx/gunzip: $!";
      $! = 0;
      if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
          print {$transcript} "$description is not available.\n";
          $ok++; return;
      } elsif ($?) {
          print {$transcript} "Error getting $description (code $? $!):\n$doc\n";
          return;
      }
    } elsif ($wherefrom eq "local") {
      open P, "$path";
      $doc = do { local $/; <P> };
      close P;
    } else {
      print {$transcript} "internal errror: info files location unknown.\n";
      $ok++; return;
    }
    print {$transcript} "Sending $description.\n";
    &sendmailmessage(<<END.$doc,$replyto);
From: "$gProject $gBug Tracking System" <$gMaintainerEmail>
To: $replyto
Subject: $gProject $gBugs information: $description
References: $header{'message-id'}
In-Reply-To: $header{'message-id'}
Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
Precedence: bulk
X-$gProject-PR-Message: getinfo

$description follows:

END
    $ok++;
    print {$transcript} "\n";
}
