]> git.donarmstrong.com Git - debbugs.git/blob - scripts/service
f726417a5e715e80bb2253d563fe1f83ba2085ff
[debbugs.git] / scripts / service
1 #!/usr/bin/perl
2 # $Id: service.in,v 1.118 2005/10/19 01:22:14 don Exp $
3 #
4 # Usage: service <code>.nn
5 # Temps:  incoming/P<code>.nn
6
7 use warnings;
8 use strict;
9
10
11 use Debbugs::Config qw(:globals :config);
12
13 use File::Copy;
14 use MIME::Parser;
15
16 use Params::Validate qw(:types validate_with);
17
18 use Debbugs::Common qw(:util :quit :misc :lock);
19
20 use Debbugs::Status qw(:read :status :write :versions :hook);
21
22 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
23 use Debbugs::Mail qw(send_mail_message);
24 use Debbugs::User;
25 use Debbugs::Recipients qw(:all);
26 use HTML::Entities qw(encode_entities);
27 use Debbugs::Versions::Dpkg;
28
29 use Debbugs::Status qw(splitpackages);
30
31 use Debbugs::CGI qw(html_escape);
32 use Debbugs::Control qw(:all);
33 use Debbugs::Log qw(:misc);
34 use Debbugs::Text qw(:templates);
35
36 use Scalar::Util qw(looks_like_number);
37
38 use List::Util qw(first);
39
40 use Mail::RFC822::Address;
41
42 chdir($config{spool_dir}) or
43      die "Unable to chdir to spool_dir '$config{spool_dir}': $!";
44
45 my $debug = 0;
46 umask(002);
47
48 my ($nn,$control) = $ARGV[0] =~ m/^(([RC])\.\d+)$/;
49 if (not defined $control or not defined $nn) {
50      die "Bad argument to service.in";
51 }
52 if (!rename("incoming/G$nn","incoming/P$nn")) {
53     defined $! and $! =~ m/no such file or directory/i and exit 0;
54     die "Failed to rename incoming/G$nn to incoming/P$nn: $!";
55 }
56
57 my $log_fh = IO::File->new("incoming/P$nn",'r') or
58      die "Unable to open incoming/P$nn for reading: $!";
59 my @log=<$log_fh>;
60 my @msg=@log;
61 close($log_fh);
62
63 chomp @msg;
64
65 print "###\n",join("##\n",@msg),"\n###\n" if $debug;
66
67 # Bug numbers to send e-mail to, hash so that we don't send to the
68 # same bug twice.
69 my (%bug_affected);
70
71 my (@headerlines,@bodylines);
72
73 my $parse_output = Debbugs::MIME::parse(join('',@log));
74 @headerlines = @{$parse_output->{header}};
75 @bodylines = @{$parse_output->{body}};
76
77 my %header;
78 for (@headerlines) {
79     $_ = decode_rfc1522($_);
80     s/\n\s/ /g;
81     print ">$_<\n" if $debug;
82     if (s/^(\S+):\s*//) {
83         my $v = lc $1;
84         print ">$v=$_<\n" if $debug;
85         $header{$v} = $_;
86     } else {
87         print "!>$_<\n" if $debug;
88     }
89 }
90 $header{'message-id'} ||= '';
91 $header{subject} ||= '';
92
93 grep(s/\s+$//,@bodylines);
94
95 print "***\n",join("\n",@bodylines),"\n***\n" if $debug;
96
97 if (defined $header{'resent-from'} && !defined $header{'from'}) {
98     $header{'from'} = $header{'resent-from'};
99 }
100
101 defined($header{'from'}) || die "no From header";
102
103 delete $header{'reply-to'} 
104         if ( defined($header{'reply-to'}) && $header{'reply-to'} =~ m/^\s*$/ );
105
106 my $replyto;
107 if ( defined($header{'reply-to'}) && $header{'reply-to'} ne "" ) {
108     $replyto = $header{'reply-to'};
109 } else {
110     $replyto = $header{'from'};
111 }
112
113 # This is an error counter which should be incremented every time there is an error.
114 my $errors = 0;
115 my $controlrequestaddr= ($control ? 'control' : 'request').'@'.$config{email_domain};
116 my $transcript_scalar = '';
117 my $transcript = IO::Scalar->new(\$transcript_scalar) or
118      die "Unable to create new IO::Scalar";
119 print {$transcript} "Processing commands for $controlrequestaddr:\n\n";
120
121
122 my $dl = 0;
123 my %affected_packages;
124 my %recipients;
125 # this is the hashref which is passed to all control calls
126 my %limit = ();
127
128
129 my @common_control_options =
130     (transcript        => $transcript,
131      requester         => $header{from},
132      request_addr      => $controlrequestaddr,
133      request_msgid     => $header{'message-id'},
134      request_subject   => $header{subject},
135      request_nn        => $nn,
136      request_replyto   => $replyto,
137      message           => \@log,
138      affected_bugs     => \%bug_affected,
139      affected_packages => \%affected_packages,
140      recipients        => \%recipients,
141      limit             => \%limit,
142     );
143
144 my $state= 'idle';
145 my $lowstate= 'idle';
146 my $mergelowstate= 'idle';
147 my $midix=0;
148
149 my $user = $replyto;
150 $user =~ s/,.*//;
151 $user =~ s/^.*<(.*)>.*$/$1/;
152 $user =~ s/[(].*[)]//;
153 $user =~ s/^\s*(\S+)\s+.*$/$1/;
154 $user = "" unless (Debbugs::User::is_valid_user($user));
155 my $indicated_user = 0;
156
157 my $quickabort = 0;
158
159
160 if (@gExcludeFromControl and grep {$replyto =~ m/\Q$_\E/} @gExcludeFromControl) {
161         print {$transcript} fill_template('mail/excluded_from_control');
162         $quickabort = 1;
163 }
164
165 my %limit_pkgs = ();
166 my %clonebugs = ();
167 my %bcc = ();
168
169
170 my @bcc;
171 sub addbcc {
172     push @bcc, $_[0] unless grep { $_ eq $_[0] } @bcc;
173 }
174
175 our $data;
176 our $message;
177 our $extramessage;
178 our $ref;
179
180 our $mismatch;
181 our $action;
182
183
184 my $ok = 0;
185 my $unknowns = 0;
186 my $procline=0;
187 for ($procline=0; $procline<=$#bodylines; $procline++) {
188     my $noriginator;
189     my $newsubmitter;
190     my $oldsubmitter;
191     my $newowner;
192     $state eq 'idle' || print "state: $state ?\n";
193     $lowstate eq 'idle' || print "lowstate: $lowstate ?\n";
194     $mergelowstate eq 'idle' || print "mergelowstate: $mergelowstate ?\n";
195     if ($quickabort) {
196          print {$transcript} "Stopping processing here.\n\n";
197          last;
198     }
199     $_= $bodylines[$procline]; s/\s+$//;
200     # Remove BOM markers from UTF-8 strings
201     # Fixes #488554
202     s/\xef\xbb\xbf//g;
203     next unless m/\S/;
204     print {$transcript} "> $_\n";
205     next if m/^\s*\#/;
206     $action= '';
207     if (m/^(?:stop|quit|--|thank(?:s|\s*you)?|kthxbye)\.*\s*$/i) {
208         print {$transcript} "Stopping processing here.\n\n";
209         last;
210     } elsif (m/^debug\s+(\d+)$/i && $1 >= 0 && $1 <= 1000) {
211         $dl= $1+0;
212         if ($dl > 0 and not grep /debug/,@common_control_options) {
213             push @common_control_options,(debug => $transcript);
214         }
215         print {$transcript} "Debug level $dl.\n\n";
216     } elsif (m/^(send|get)\s+\#?(\d{2,})$/i) {
217         $ref= $2+0;
218         &sendlynxdoc("bugreport.cgi?bug=$ref","logs for $gBug#$ref");
219     } elsif (m/^send-detail\s+\#?(\d{2,})$/i) {
220         $ref= $1+0;
221         &sendlynxdoc("bugreport.cgi?bug=$ref&boring=yes",
222                      "detailed logs for $gBug#$ref");
223     } elsif (m/^index(\s+full)?$/i) {
224         print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
225         $errors++;
226         $ok++; # well, it's not really ok, but it fixes #81224 :)
227     } elsif (m/^index-summary\s+by-package$/i) {
228         print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
229         $errors++;
230         $ok++; # well, it's not really ok, but it fixes #81224 :)
231     } elsif (m/^index-summary(\s+by-number)?$/i) {
232         print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
233         $errors++;
234         $ok++; # well, it's not really ok, but it fixes #81224 :)
235     } elsif (m/^index(\s+|-)pack(age)?s?$/i) {
236         &sendlynxdoc("pkgindex.cgi?indexon=pkg",'index of packages');
237     } elsif (m/^index(\s+|-)maints?$/i) {
238         &sendlynxdoc("pkgindex.cgi?indexon=maint",'index of maintainers');
239     } elsif (m/^index(\s+|-)maint\s+(\S+)$/i) {
240         my $maint = $2;
241         &sendlynxdoc("pkgreport.cgi?maint=" . urlsanit($maint),
242                      "$gBug list for maintainer \`$maint'");
243         $ok++;
244     } elsif (m/^index(\s+|-)pack(age)?s?\s+(\S.*\S)$/i) {
245         my $package = $+;
246         &sendlynxdoc("pkgreport.cgi?pkg=" . urlsanit($package),
247                      "$gBug list for package $package");
248         $ok++;
249     } elsif (m/^send-unmatched(\s+this|\s+-?0)?$/i) {
250         print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
251         $errors++;
252         $ok++; # well, it's not really ok, but it fixes #81224 :)
253     } elsif (m/^send-unmatched\s+(last|-1)$/i) {
254         print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
255         $errors++;
256         $ok++; # well, it's not really ok, but it fixes #81224 :)
257     } elsif (m/^send-unmatched\s+(old|-2)$/i) {
258         print {$transcript} "This BTS function is currently disabled, sorry.\n\n";
259         $errors++;
260         $ok++; # well, it's not really ok, but it fixes #81224 :)
261     } elsif (m/^getinfo\s+([\w.-]+)$/i) {
262         # the following is basically a Debian-specific kludge, but who cares
263         my $req = $1;
264         if ($req =~ /^maintainers$/i && -f "$gConfigDir/Maintainers") {
265             &sendinfo("local", "$gConfigDir/Maintainers", "Maintainers file");
266         } elsif ($req =~ /^override\.(\w+)\.([\w.-]+)$/i) {
267             $req =~ s/.gz$//;
268             &sendinfo("ftp.d.o", "$req", "override file for $2 part of $1 distribution");
269         } elsif ($req =~ /^pseudo-packages\.(description|maintainers)$/i && -f "$gConfigDir/$req") {
270             &sendinfo("local", "$gConfigDir/$req", "$req file");
271         } else {
272             print {$transcript} "Info file $req does not exist.\n\n";
273         }
274     } elsif (m/^help/i) {
275         &sendhelp;
276         print {$transcript} "\n";
277         $ok++;
278     } elsif (m/^refcard/i) {
279         &sendtxthelp("bug-mailserver-refcard.txt","mail servers' reference card");
280     } elsif (m/^subscribe/i) {
281         print {$transcript} <<END;
282 There is no $gProject $gBug mailing list.  If you wish to review bug reports
283 please do so via http://$gWebDomain/ or ask this mail server
284 to send them to you.
285 soon: MAILINGLISTS_TEXT
286 END
287     } elsif (m/^unsubscribe/i) {
288         print {$transcript} <<END;
289 soon: UNSUBSCRIBE_TEXT
290 soon: MAILINGLISTS_TEXT
291 END
292     } elsif (m/^user\s+(\S+)\s*$/i) {
293         my $newuser = $1;
294         if (Debbugs::User::is_valid_user($newuser)) {
295             my $olduser = ($user ne "" ? " (was $user)" : "");
296             print {$transcript} "Setting user to $newuser$olduser.\n";
297             $user = $newuser;
298             $indicated_user = 1;
299         } else {
300             print {$transcript} "Selected user id ($newuser) invalid, sorry\n";
301             $errors++;
302             $user = "";
303             $indicated_user = 1;
304         }
305     } elsif (m/^usercategory\s+(\S+)(\s+\[hidden\])?\s*$/i) {
306         $ok++;
307         my $catname = $1;
308         my $hidden = (defined $2 and $2 ne "");
309
310         my $prefix = "";
311         my @cats;
312         my $bad = 0;
313         my $catsec = 0;
314         if ($user eq "") {
315             print {$transcript} "No valid user selected\n";
316             $errors++;
317             next;
318         }
319         if (not $indicated_user and defined $user) {
320              print {$transcript} "User is $user\n";
321              $indicated_user = 1;
322         }
323         my @ords = ();
324         while (++$procline <= $#bodylines) {
325             unless ($bodylines[$procline] =~ m/^\s*([*+])\s*(\S.*)$/) {
326                 $procline--;
327                 last;
328             }
329             print {$transcript} "> $bodylines[$procline]\n";
330             next if $bad;
331             my ($o, $txt) = ($1, $2);
332             if ($#cats == -1 && $o eq "+") {
333                 print {$transcript} "User defined category specification must start with a category name. Skipping.\n\n";
334                 $errors++;
335                 $bad = 1;
336                 next;
337             }
338             if ($o eq "+") {
339                 unless (ref($cats[-1]) eq "HASH") {
340                     $cats[-1] = { "nam" => $cats[-1], 
341                                   "pri" => [], "ttl" => [] };
342                 }
343                 $catsec++;
344                 my ($desc, $ord, $op);
345                 if ($txt =~ m/^(.*\S)\s*\[((\d+):\s*)?\]\s*$/) {
346                     $desc = $1; $ord = $3; $op = "";
347                 } elsif ($txt =~ m/^(.*\S)\s*\[((\d+):\s*)?(\S+)\]\s*$/) {
348                     $desc = $1; $ord = $3; $op = $4;
349                 } elsif ($txt =~ m/^([^[\s]+)\s*$/) {
350                     $desc = ""; $op = $1;
351                 } else {
352                     print {$transcript} "Unrecognised syntax for category section. Skipping.\n\n";
353                     $errors++;
354                     $bad = 1;
355                     next;
356                 }
357                 $ord = 999 unless defined $ord;
358
359                 if ($op) {
360                     push @{$cats[-1]->{"pri"}}, $prefix . $op;
361                     push @{$cats[-1]->{"ttl"}}, $desc;
362                     push @ords, "$ord $catsec";
363                 } else {
364                     $cats[-1]->{"def"} = $desc;
365                     push @ords, "$ord DEF";
366                     $catsec--;
367                 }
368                 @ords = sort {
369                     my ($a1, $a2, $b1, $b2) = split / /, "$a $b";
370                     ((looks_like_number($a1) and looks_like_number($a2))?$a1 <=> $b1:$a1 cmp $b1) ||
371                     ((looks_like_number($a2) and looks_like_number($b2))?$a2 <=> $b2:$a2 cmp $b2);
372                 } @ords;
373                 $cats[-1]->{"ord"} = [map { m/^.* (\S+)/; $1 eq "DEF" ? $catsec + 1 : $1 } @ords];
374             } elsif ($o eq "*") {
375                 $catsec = 0;
376                 my ($name);
377                 if ($txt =~ m/^(.*\S)(\s*\[(\S+)\])\s*$/) {
378                     $name = $1; $prefix = $3;
379                 } else {
380                     $name = $txt; $prefix = "";
381                 }
382                 push @cats, $name;
383             }
384         }
385         # XXX: got @cats, now do something with it
386         my $u = Debbugs::User::get_user($user);
387         if (@cats) {
388             print {$transcript} "Added usercategory $catname.\n\n";
389             $u->{"categories"}->{$catname} = [ @cats ];
390             if (not $hidden) {
391                  push @{$u->{visible_cats}},$catname;
392             }
393         } else {
394             print {$transcript} "Removed usercategory $catname.\n\n";
395             delete $u->{"categories"}->{$catname};
396             @{$u->{visible_cats}} = grep {$_ ne $catname} @{$u->{visible_cats}};
397         }
398         $u->write();
399     } elsif (m/^usertags?\s+\#?(-?\d+)\s+(([=+-])\s*)?(\S.*)?$/i) {
400         $ok++;
401         $ref = $1;
402         my $addsubcode = $3 || "+";
403         my $tags = $4;
404         if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) {
405              $ref = $clonebugs{$ref};
406         }
407         if ($user eq "") {
408             print {$transcript} "No valid user selected\n";
409             $errors++;
410             $indicated_user = 1;
411         } elsif (&setbug) {
412             if (not $indicated_user and defined $user) {
413                  print {$transcript} "User is $user\n";
414                  $indicated_user = 1;
415             }
416             &nochangebug;
417             my %ut;
418             Debbugs::User::read_usertags(\%ut, $user);
419             my @oldtags = (); my @newtags = (); my @badtags = ();
420             my %chtags;
421             if (defined $tags and length $tags) {
422                  for my $t (split /[,\s]+/, $tags) {
423                       if ($t =~ m/^[a-zA-Z0-9.+\@-]+$/) {
424                            $chtags{$t} = 1;
425                       } else {
426                            push @badtags, $t;
427                       }
428                  }
429             }
430             if (@badtags) {
431                 print {$transcript} "Ignoring illegal tag/s: ".join(', ', @badtags).".\nPlease use only alphanumerics, at, dot, plus and dash.\n";
432                 $errors++;
433             }
434             for my $t (keys %chtags) {
435                 $ut{$t} = [] unless defined $ut{$t};
436             }
437             for my $t (keys %ut) {
438                 my %res = map { ($_, 1) } @{$ut{$t}};
439                 push @oldtags, $t if defined $res{$ref};
440                 my $addop = ($addsubcode eq "+" or $addsubcode eq "=");
441                 my $del = (defined $chtags{$t} ? $addsubcode eq "-" 
442                                                : $addsubcode eq "=");
443                 $res{$ref} = 1 if ($addop && defined $chtags{$t});
444                 delete $res{$ref} if ($del);
445                 push @newtags, $t if defined $res{$ref};
446                 $ut{$t} = [ sort { $a <=> $b } (keys %res) ];
447             }
448             if (@oldtags == 0) {
449                 print {$transcript} "There were no usertags set.\n";
450             } else {
451                 print {$transcript} "Usertags were: " . join(" ", @oldtags) . ".\n";
452             }
453             print {$transcript} "Usertags are now: " . join(" ", @newtags) . ".\n";
454             Debbugs::User::write_usertags(\%ut, $user);
455         }
456     } elsif (!$control) {
457         print {$transcript} <<END;
458 Unknown command or malformed arguments to command.
459 (Use control\@$gEmailDomain to manipulate reports.)
460
461 END
462         $errors++;
463         if (++$unknowns >= 3) {
464             print {$transcript} "Too many unknown commands, stopping here.\n\n";
465             last;
466         }
467 #### "developer only" ones start here
468     } elsif (m/^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/i) {
469         $ok++;
470         $ref= $1;
471         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
472         if (defined $2) {
473             eval {
474                 set_fixed(@common_control_options,
475                           bug   => $ref,
476                           fixed => $2,
477                           add   => 1,
478                          );
479             };
480             if ($@) {
481                 $errors++;
482                 print {$transcript} "Failed to add fixed version '$2' to $ref: ".cleanup_eval_fail($@,$debug)."\n";
483             }
484         }
485         eval {
486             set_done(@common_control_options,
487                      done      => 1,
488                      bug       => $ref,
489                      reopen    => 0,
490                      notify_submitter => 1,
491                      clear_fixed => 0,
492                     );
493         };
494         if ($@) {
495             $errors++;
496             print {$transcript} "Failed to mark $ref as done: ".cleanup_eval_fail($@,$debug)."\n";
497         }
498     } elsif (m/^reassign\s+\#?(-?\d+)\s+ # bug and command
499                (?:(?:((?:src:|source:)?$config{package_name_re}) # new package
500                (?:\s+((?:$config{package_name_re}\/)?
501                        $config{package_version_re}))?)| # optional version
502                ((?:src:|source:)?$config{package_name_re} # multiple package form
503                (?:\s*\,\s*(?:src:|source:)?$config{package_name_re})+))
504                \s*$/xi) {
505         $ok++;
506         $ref= $1;
507         my @new_packages;
508         if (not defined $2) {
509             push @new_packages, split /\s*\,\s*/,$4;
510         }
511         else {
512             push @new_packages, $2;
513         }
514         @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages;
515         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
516         my $version= $3;
517         eval {
518             set_package(@common_control_options,
519                         bug          => $ref,
520                         package      => \@new_packages,
521                        );
522             # if there is a version passed, we make an internal call
523             # to set_found
524             if (defined($version) && length $version) {
525                 set_found(@common_control_options,
526                           bug   => $ref,
527                           found => $version,
528                          );
529             }
530         };
531         if ($@) {
532             $errors++;
533             print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
534         }
535     } elsif (m/^reopen\s+\#?(-?\d+)(?:\s+([\=\!]|(?:\S.*\S)))?$/i) {
536         $ok++;
537         $ref= $1;
538         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
539         my $new_submitter = $2;
540         if (defined $new_submitter) {
541             if ($new_submitter eq '=') {
542                 undef $new_submitter;
543             }
544             elsif ($new_submitter eq '!') {
545                 $new_submitter = $replyto;
546             }
547         }
548         eval {
549             set_done(@common_control_options,
550                      bug          => $ref,
551                      reopen       => 1,
552                      submitter    => $new_submitter,
553                     );
554         };
555         if ($@) {
556             $errors++;
557             print {$transcript} "Failed to reopen $ref: ".cleanup_eval_fail($@,$debug)."\n";
558         }
559     } elsif (m{^(?:(?i)found)\s+\#?(-?\d+)
560                (?:\s+((?:$config{package_name_re}\/)?
561                     $config{package_version_re}
562                 # allow for multiple packages
563                 (?:\s*,\s*(?:$config{package_name_re}\/)?
564                     $config{package_version_re})*)
565             )?$}x) {
566         $ok++;
567         $ref= $1;
568         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
569         my @versions;
570         if (defined $2) {
571             @versions = split /\s*,\s*/,$2;
572             eval {
573                 set_found(@common_control_options,
574                           bug          => $ref,
575                           found        => \@versions,
576                           add          => 1,
577                          );
578             };
579             if ($@) {
580                 $errors++;
581                 print {$transcript} "Failed to add found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
582             }
583         }
584         else {
585             eval {
586                 set_fixed(@common_control_options,
587                           bug          => $ref,
588                           fixed        => [],
589                           reopen       => 1,
590                          );
591             };
592             if ($@) {
593                 $errors++;
594                 print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
595             }
596         }
597     }
598     elsif (m{^(?:(?i)notfound)\s+\#?(-?\d+)
599              \s+((?:$config{package_name_re}\/)?
600                  $config{package_version_re}
601                 # allow for multiple packages
602                 (?:\s*,\s*(?:$config{package_name_re}\/)?
603                     $config{package_version_re})*
604             )$}x) {
605         $ok++;
606         $ref= $1;
607         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
608         my @versions;
609         @versions = split /\s*,\s*/,$2;
610         eval {
611             set_found(@common_control_options,
612                       bug          => $ref,
613                       found        => \@versions,
614                       remove       => 1,
615                      );
616         };
617         if ($@) {
618             $errors++;
619             print {$transcript} "Failed to remove found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
620         }
621     }
622     elsif (m{^(?:(?i)fixed)\s+\#?(-?\d+)
623              \s+((?:$config{package_name_re}\/)?
624                     $config{package_version_re}
625                 # allow for multiple packages
626                 (?:\s*,\s*(?:$config{package_name_re}\/)?
627                     $config{package_version_re})*)
628             \s*$}x) {
629         $ok++;
630         $ref= $1;
631         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
632         my @versions;
633         @versions = split /\s*,\s*/,$2;
634         eval {
635             set_fixed(@common_control_options,
636                       bug          => $ref,
637                       fixed        => \@versions,
638                       add          => 1,
639                      );
640         };
641         if ($@) {
642             $errors++;
643             print {$transcript} "Failed to add fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
644         }
645     }
646     elsif (m{^(?:(?i)notfixed)\s+\#?(-?\d+)
647              \s+((?:$config{package_name_re}\/)?
648                     $config{package_version_re}
649                 # allow for multiple packages
650                 (?:\s*,\s*(?:$config{package_name_re}\/)?
651                     $config{package_version_re})*)
652             \s*$}x) {
653         $ok++;
654         $ref= $1;
655         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
656         my @versions;
657         @versions = split /\s*,\s*/,$2;
658         eval {
659             set_fixed(@common_control_options,
660                       bug          => $ref,
661                       fixed        => \@versions,
662                       remove       => 1,
663                      );
664         };
665         if ($@) {
666             $errors++;
667             print {$transcript} "Failed to remove fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
668         }
669     }
670     elsif (m/^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/i) {
671         $ok++;
672         $ref= $1;
673         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
674         my $newsubmitter = $2 eq '!' ? $replyto : $2;
675         if (not Mail::RFC822::Address::valid($newsubmitter)) {
676              print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n";
677              $errors++;
678         }
679         else {
680             eval {
681                 set_submitter(@common_control_options,
682                               bug       => $ref,
683                               submitter => $newsubmitter,
684                              );
685             };
686             if ($@) {
687                 $errors++;
688                 print {$transcript} "Failed to set submitter on $ref: ".cleanup_eval_fail($@,$debug)."\n";
689             }
690         }
691     } elsif (m/^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/i) {
692         $ok++;
693         $ref= $1;
694         my $forward_to= $2;
695         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
696         eval {
697             set_forwarded(@common_control_options,
698                           bug          => $ref,
699                           forwarded    => $forward_to,
700                           );
701         };
702         if ($@) {
703             $errors++;
704             print {$transcript} "Failed to set the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
705         }
706     } elsif (m/^notforwarded\s+\#?(-?\d+)$/i) {
707         $ok++;
708         $ref= $1;
709         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
710         eval {
711             set_forwarded(@common_control_options,
712                           bug          => $ref,
713                           forwarded    => undef,
714                           );
715         };
716         if ($@) {
717             $errors++;
718             print {$transcript} "Failed to clear the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
719         }
720     } elsif (m/^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) {
721         $ok++;
722         $ref= $1;
723         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
724         my $newseverity= $2;
725         if (exists $gObsoleteSeverities{$newseverity}) {
726             print {$transcript} "Severity level \`$newseverity' is obsolete. " .
727                  "Use $gObsoleteSeverities{$newseverity} instead.\n\n";
728                 $errors++;
729         } elsif (not defined first {$_ eq $newseverity}
730             (@gSeverityList, "$gDefaultSeverity")) {
731              print {$transcript} "Severity level \`$newseverity' is not known.\n".
732                   "Recognized are: $gShowSeverities.\n\n";
733             $errors++;
734         } else {
735             eval {
736                 set_severity(@common_control_options,
737                              bug => $ref,
738                              severity => $newseverity,
739                             );
740             };
741             if ($@) {
742                 $errors++;
743                 print {$transcript} "Failed to set severity of $config{bug} $ref to $newseverity: ".cleanup_eval_fail($@,$debug)."\n";
744             }
745         }
746     } elsif (m/^tags?\s+\#?(-?\d+)\s+(\S.*)$/i) {
747         $ok++;
748         $ref = $1;
749         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
750         my $tags = $2;
751         my @tags = map {m/^([+=-])(.+)/ ? ($1,$2):($_)} split /[\s,]+/, $tags;
752         # this is an array of hashrefs which contain two elements, the
753         # first of which is the array of tags, the second is the
754         # option to pass to set_tag (we use a hashref here to make it
755         # more obvious what is happening)
756         my @tag_operations;
757         my @badtags;
758         for my $tag (@tags) {
759             if ($tag =~ /^[=+-]$/) {
760                 if ($tag eq '=') {
761                     @tag_operations = {tags => [],
762                                        option => [],
763                                       };
764                 }
765                 elsif ($tag eq '-') {
766                     push @tag_operations,
767                         {tags => [],
768                          option => [remove => 1],
769                         };
770                 }
771                 elsif ($tag eq '+') {
772                     push @tag_operations,
773                         {tags => [],
774                          option => [add => 1],
775                         };
776                 }
777                 next;
778             }
779             if (not defined first {$_ eq $tag} @{$config{tags}}) {
780                 push @badtags, $tag;
781                 next;
782             }
783             if (not @tag_operations) {
784                 @tag_operations = {tags => [],
785                                    option => [add => 1],
786                                   };
787             }
788             push @{$tag_operations[-1]{tags}},$tag;
789         }
790         if (@badtags) {
791             print {$transcript} "Unknown tag/s: ".join(', ', @badtags).".\n".
792                  "Recognized are: ".join(' ', @gTags).".\n\n";
793             $errors++;
794         }
795         eval {
796             for my $operation (@tag_operations) {
797                 set_tag(@common_control_options,
798                         bug => $ref,
799                         tag => [@{$operation->{tags}}],
800                         warn_on_bad_tags => 0, # don't warn on bad tags,
801                         # 'cause we do that above
802                         @{$operation->{option}},
803                        );
804             }
805         };
806         if ($@) {
807             # we intentionally have two errors here if there is a bad
808             # tag and the above fails for some reason
809             $errors++;
810             print {$transcript} "Failed to alter tags of $config{bug} $ref: ".cleanup_eval_fail($@,$debug)."\n";
811         }
812     } elsif (m/^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/i) {
813         $ok++;
814         $ref= $2;
815         my $add_remove = defined $1 && $1 eq 'un';
816         my @blockers = map {exists $clonebugs{$_}?$clonebugs{$_}:$_} split /[\s,]+/, $3;
817         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
818         eval {
819              set_blocks(@common_control_options,
820                         bug          => $ref,
821                         block        => \@blockers,
822                         $add_remove ? (remove => 1):(add => 1),
823                        );
824         };
825         if ($@) {
826             $errors++;
827             print {$transcript} "Failed to set blocking bugs of $ref: ".cleanup_eval_fail($@,$debug)."\n";
828         }
829     } elsif (m/^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/i) {
830         $ok++;
831         $ref= $1; my $newtitle= $2;
832         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
833         eval {
834              set_title(@common_control_options,
835                        bug          => $ref,
836                        title        => $newtitle,
837                       );
838         };
839         if ($@) {
840             $errors++;
841             print {$transcript} "Failed to set the title of $ref: ".cleanup_eval_fail($@,$debug)."\n";
842         }
843     } elsif (m/^unmerge\s+\#?(-?\d+)$/i) {
844         $ok++;
845         $ref= $1;
846         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
847         eval {
848              set_merged(@common_control_options,
849                         bug          => $ref,
850                        );
851         };
852         if ($@) {
853             $errors++;
854             print {$transcript} "Failed to unmerge $ref: $@".cleanup_eval_fail($@,$debug)."\n";
855         }
856     } elsif (m/^merge\s+#?(-?\d+(\s+#?-?\d+)+)\s*$/i) {
857         $ok++;
858         my @tomerge;
859         ($ref,@tomerge) = map {exists $clonebugs{$_}?$clonebugs{$_}:$_}
860             split(/\s+#?/,$1);
861         eval {
862              set_merged(@common_control_options,
863                         bug          => $ref,
864                         merge_with   => \@tomerge,
865                        );
866         };
867         if ($@) {
868             $errors++;
869             print {$transcript} "Failed to merge $ref: $@".cleanup_eval_fail($@,$debug)."\n";
870         }
871     } elsif (m/^forcemerge\s+\#?(-?\d+(?:\s+\#?-?\d+)+)\s*$/i) {
872         $ok++;
873         my @tomerge;
874         ($ref,@tomerge) = map {exists $clonebugs{$_}?$clonebugs{$_}:$_}
875             split(/\s+#?/,$1);
876         eval {
877              set_merged(@common_control_options,
878                         bug          => $ref,
879                         merge_with   => \@tomerge,
880                         force        => 1,
881                         masterbug    => 1,
882                        );
883         };
884         if ($@) {
885             $errors++;
886             print {$transcript} $@;
887             print {$transcript} "Failed to forcibly merge $ref: ".cleanup_eval_fail($@,$debug)."\n";
888         }
889     } elsif (m/^clone\s+#?(\d+)\s+((-\d+\s+)*-\d+)\s*$/i) {
890         $ok++;
891
892         my $origref = $1;
893         my @newclonedids = split /\s+/, $2;
894         my $newbugsneeded = scalar(@newclonedids);
895
896         $ref = $origref;
897         $bug_affected{$ref} = 1;
898         if (&setbug) {
899             $affected_packages{$data->{package}} = 1;
900             if (length($data->{mergedwith})) {
901                 print {$transcript} "$gBug is marked as being merged with others. Use an existing clone.\n\n";
902                 $errors++;
903                 &nochangebug;
904             } else {
905                 &filelock("nextnumber.lock");
906                 open(N,"nextnumber") || die "nextnumber: read: $!";
907                 my $v=<N>; $v =~ s/\n$// || die "nextnumber bad format";
908                 my $firstref= $v+0;  $v += $newbugsneeded;
909                 open(NN,">nextnumber"); print NN "$v\n"; close(NN);
910                 unfilelock();
911
912                 my $lastref = $firstref + $newbugsneeded - 1;
913
914                 if ($newbugsneeded == 1) {
915                     $action= "$gBug $origref cloned as bug $firstref.";
916                 } else {
917                     $action= "$gBug $origref cloned as bugs $firstref-$lastref.";
918                 }
919
920                 my $blocks = $data->{blocks};
921                 my $blockedby = $data->{blockedby};
922                 
923                 &getnextbug;
924                 my $ohash = get_hashname($origref);
925                 my $clone = $firstref;
926                 @bug_affected{@newclonedids} = 1 x @newclonedids;
927                 for my $newclonedid (@newclonedids) {
928                     $clonebugs{$newclonedid} = $clone;
929             
930                     my $hash = get_hashname($clone);
931                     copy("db-h/$ohash/$origref.log", "db-h/$hash/$clone.log");
932                     copy("db-h/$ohash/$origref.status", "db-h/$hash/$clone.status");
933                     copy("db-h/$ohash/$origref.summary", "db-h/$hash/$clone.summary");
934                     copy("db-h/$ohash/$origref.report", "db-h/$hash/$clone.report");
935                     &bughook('new', $clone, $data);
936                 
937                     # Update blocking info of bugs blocked by or blocking the
938                     # cloned bug.
939                     foreach $ref (split ' ', $blocks) {
940                         &getbug;
941                         $data->{blockedby} = manipset($data->{blockedby}, $clone, 1);
942                         &savebug;
943                     }
944                     foreach $ref (split ' ', $blockedby) {
945                         &getbug;
946                         $data->{blocks} = manipset($data->{blocks}, $clone, 1);
947                         &savebug;
948                     }
949
950                     $clone++;
951                 }
952             }
953         }
954     } elsif (m/^package\:?\s+(\S.*\S)?\s*$/i) {
955         $ok++;
956         my @pkgs = split /\s+/, $1;
957         if (scalar(@pkgs) > 0) {
958                 %limit_pkgs = map { ($_, 1) } @pkgs;
959                 $limit{package} = [@pkgs];
960                 print {$transcript} "Limiting to bugs with field 'package' containing at least one of ".join(', ',map {qq('$_')} @pkgs)."\n";
961                 print {$transcript} "Limit currently set to";
962                 for my $limit_field (keys %limit) {
963                     print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n";
964                 }
965                 print {$transcript} "\n";
966         } else {
967             %limit_pkgs = ();
968             $limit{package} = [];
969             print {$transcript} "Limit cleared.\n\n";
970         }
971     } elsif (m/^limit\:?\s+(\S.*\S)\s*$/) {
972         $ok++;
973         my ($field,@options) = split /\s+/, $1;
974         $field = lc($field);
975         if ($field =~ /^(?:clear|unset|blank)$/) {
976             %limit = ();
977             print {$transcript} "Limit cleared.\n\n";
978         }
979         elsif (exists $Debbugs::Status::fields{$field} or $field eq 'source') {
980             # %limit can actually contain regexes, but because they're
981             # not evaluated in Safe, DO NOT allow them through without
982             # fixing this.
983             $limit{$field} = [@options];
984             print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n";
985             print {$transcript} "Limit currently set to";
986             for my $limit_field (keys %limit) {
987                 print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n";
988             }
989             print {$transcript} "\n";
990         }
991         else {
992             print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n";
993             $errors++;
994             last;
995         }
996     } elsif (m/^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/i) {
997         $ok++;
998         $ref = $1;
999         my $add_remove = $2 || '';
1000         my $packages = $3 || '';
1001         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1002         eval {
1003              affects(@common_control_options,
1004                      bug => $ref,
1005                      package     => [splitpackages($3)],
1006                      ($add_remove eq '+'?(add => 1):()),
1007                      ($add_remove eq '-'?(remove => 1):()),
1008                     );
1009         };
1010         if ($@) {
1011             $errors++;
1012             print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n";
1013         }
1014
1015     } elsif (m/^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/i) {
1016         $ok++;
1017         $ref = $1;
1018         my $summary_msg = length($2)?$2:undef;
1019         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1020         eval {
1021             summary(@common_control_options,
1022                     bug          => $ref,
1023                     summary      => $summary_msg,
1024                    );
1025         };
1026         if ($@) {
1027             $errors++;
1028             print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n";
1029         }
1030
1031     } elsif (m/^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/i) {
1032         $ok++;
1033         $ref = $1;
1034         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1035         my $newowner = $2;
1036         if ($newowner eq '!') {
1037             $newowner = $replyto;
1038         }
1039         eval {
1040             owner(@common_control_options,
1041                   bug          => $ref,
1042                   owner        => $newowner,
1043                  );
1044         };
1045         if ($@) {
1046             $errors++;
1047             print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n";
1048         }
1049     } elsif (m/^noowner\s+\#?(-?\d+)\s*$/i) {
1050         $ok++;
1051         $ref = $1;
1052         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1053         eval {
1054             owner(@common_control_options,
1055                   bug          => $ref,
1056                   owner        => undef,
1057                  );
1058         };
1059         if ($@) {
1060             $errors++;
1061             print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n";
1062         }
1063     } elsif (m/^unarchive\s+#?(\d+)$/i) {
1064          $ok++;
1065          $ref = $1;
1066          $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1067          eval {
1068               bug_unarchive(@common_control_options,
1069                             bug        => $ref,
1070                             recipients => \%recipients,
1071                            );
1072          };
1073          if ($@) {
1074               $errors++;
1075          }
1076     } elsif (m/^archive\s+#?(\d+)$/i) {
1077          $ok++;
1078          $ref = $1;
1079          $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1080          eval {
1081               bug_archive(@common_control_options,
1082                           bug => $ref,
1083                           ignore_time => 1,
1084                           archive_unarchived => 0,
1085                          );
1086          };
1087          if ($@) {
1088               $errors++;
1089          }
1090     } else {
1091         print {$transcript} "Unknown command or malformed arguments to command.\n\n";
1092         $errors++;
1093         if (++$unknowns >= 5) {
1094             print {$transcript} "Too many unknown commands, stopping here.\n\n";
1095             last;
1096         }
1097     }
1098 }
1099 if ($procline>$#bodylines) {
1100     print {$transcript} ">\nEnd of message, stopping processing here.\n\n";
1101 }
1102 if (!$ok && !$quickabort) {
1103     $errors++;
1104     print {$transcript} "No commands successfully parsed; sending the help text(s).\n";
1105     &sendhelp;
1106     print {$transcript} "\n";
1107 }
1108
1109 my @maintccs = determine_recipients(recipients => \%recipients,
1110                                     address_only => 1,
1111                                     cc => 1,
1112                                    );
1113 my $maintccs = 'Cc: '.join(",\n    ",
1114                     determine_recipients(recipients => \%recipients,
1115                                          cc => 1,
1116                                         )
1117                    )."\n";
1118
1119 my $packagepr = '';
1120 $packagepr = "X-${gProject}-PR-Package: " . join(keys %affected_packages) . "\n" if keys %affected_packages;
1121
1122 # Add Bcc's to subscribed bugs
1123 # now handled by Debbugs::Recipients
1124 #push @bcc, map {"bugs=$_\@$gListDomain"} keys %bug_affected;
1125
1126 if (!defined $header{'subject'} || $header{'subject'} eq "") {
1127   $header{'subject'} = "your mail";
1128 }
1129
1130 # Error text here advertises how many errors there were
1131 my $error_text = $errors > 0 ? " (with $errors errors)":'';
1132
1133 my $reply= <<END;
1134 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1135 To: $replyto
1136 ${maintccs}Subject: Processed${error_text}: $header{'subject'}
1137 In-Reply-To: $header{'message-id'}
1138 END
1139 $reply .= <<END;
1140 References: $header{'message-id'}
1141 Message-ID: <handler.s.$nn.transcript\@$gEmailDomain>
1142 Precedence: bulk
1143 ${packagepr}X-$gProject-PR-Message: transcript
1144 END
1145
1146 $reply .= fill_template('mail/message_body',
1147                           {body => "${transcript_scalar}Please contact me if you need assistance."},
1148                         );
1149
1150 my $repliedshow= join(', ',$replyto,
1151                       determine_recipients(recipients => \%recipients,
1152                                            cc => 1,
1153                                            address_only => 1,
1154                                           )
1155                      );
1156 # -1 is the service.in log
1157 &filelock("lock/-1");
1158 open(AP,">>db-h/-1.log") || die "open db-h/-1.log: $!";
1159 print(AP
1160       "\2\n$repliedshow\n\5\n$reply\n\3\n".
1161       "\6\n".
1162       "<strong>Request received</strong> from <code>".
1163       html_escape($header{'from'})."</code>\n".
1164       "to <code>".html_escape($controlrequestaddr)."</code>\n".
1165       "\3\n".
1166       "\7\n",escape_log(@log),"\n\3\n") || die "writing db-h/-1.log: $!";
1167 close(AP) || die "open db-h/-1.log: $!";
1168 &unfilelock;
1169 utime(time,time,"db-h");
1170
1171 &sendmailmessage($reply,
1172                  exists $header{'x-debbugs-no-ack'}?():$replyto,
1173                  make_list(values %{{determine_recipients(recipients => \%recipients,
1174                                                           address_only => 1,
1175                                                          )}}
1176                           ),
1177                 );
1178
1179 unlink("incoming/P$nn") || die "unlinking incoming/P$nn: $!";
1180
1181 sub sendmailmessage {
1182     my ($message,@recips) = @_;
1183     $message = "X-Loop: $gMaintainerEmail\n" . $message;
1184     send_mail_message(message    => $message,
1185                       recipients => \@recips,
1186                      );
1187     $midix++;
1188 }
1189
1190 sub fill_template{
1191      my ($template,$extra_var) = @_;
1192      $extra_var ||={};
1193      my $variables = {config => \%config,
1194                       defined($ref)?(ref    => $ref):(),
1195                       defined($data)?(data  => $data):(),
1196                       refs => [map {exists $clonebugs{$_}?$clonebugs{$_}:$_} keys %bug_affected],
1197                       %{$extra_var},
1198                      };
1199      my $hole_var = {'&bugurl' =>
1200                      sub{"$_[0]: ".
1201                               'http://'.$config{cgi_domain}.'/'.
1202                                    Debbugs::CGI::bug_links(bug=>$_[0],
1203                                                            links_only => 1,
1204                                                           );
1205                     }
1206                     };
1207      return fill_in_template(template => $template,
1208                              variables => $variables,
1209                              hole_var  => $hole_var,
1210                             );
1211 }
1212
1213 =head2 message_body_template
1214
1215      message_body_template('mail/ack',{ref=>'foo'});
1216
1217 Creates a message body using a template
1218
1219 =cut
1220
1221 sub message_body_template{
1222      my ($template,$extra_var) = @_;
1223      $extra_var ||={};
1224      my $body = fill_template($template,$extra_var);
1225      return fill_template('mail/message_body',
1226                           {%{$extra_var},
1227                            body => $body,
1228                           },
1229                          );
1230 }
1231
1232 sub sendhelp {
1233      if ($control) {
1234           &sendtxthelpraw("bug-maint-mailcontrol.txt","instructions for control\@$gEmailDomain")
1235      }
1236      else {
1237           &sendtxthelpraw("bug-log-mailserver.txt","instructions for request\@$gEmailDomain");
1238      }
1239 }
1240
1241 #sub unimplemented {
1242 #    print {$transcript} "Sorry, command $_[0] not yet implemented.\n\n";
1243 #}
1244 our %checkmatch_values;
1245 sub checkmatch {
1246     my ($string,$mvarname,$svarvalue,@newmergelist) = @_;
1247     my ($mvarvalue);
1248     if (@newmergelist) {
1249         $mvarvalue = $checkmatch_values{$mvarname};
1250         print {$transcript} "D| checkmatch \`$string' /$mvarname/$mvarvalue/$svarvalue/\n"
1251             if $dl;
1252         $mismatch .=
1253             "Values for \`$string' don't match:\n".
1254             " #$newmergelist[0] has \`$mvarvalue';\n".
1255             " #$ref has \`$svarvalue'\n"
1256             if $mvarvalue ne $svarvalue;
1257     } else {
1258          print {$transcript} "D| setupmatch \`$string' /$mvarname/$svarvalue/\n"
1259               if $dl;
1260          $checkmatch_values{$mvarname} = $svarvalue;
1261     }
1262 }
1263
1264 sub checkpkglimit {
1265     if (keys %limit_pkgs and not defined $limit_pkgs{$data->{package}}) {
1266         print {$transcript} "$gBug number $ref belongs to package $data->{package}, skipping.\n\n";
1267         $errors++;
1268         return 0;
1269     }
1270     return 1;
1271 }
1272
1273 sub manipset {
1274     my $list = shift;
1275     my $elt = shift;
1276     my $add = shift;
1277
1278     my %h = map { $_ => 1 } split ' ', $list;
1279     if ($add) {
1280         $h{$elt}=1;
1281     }
1282     else {
1283         delete $h{$elt};
1284     }
1285     return join ' ', sort keys %h;
1286 }
1287
1288 # High-level bug manipulation calls
1289 # Do announcements themselves
1290 #
1291 # Possible calling sequences:
1292 #    setbug (returns 0)
1293 #    
1294 #    setbug (returns 1)
1295 #    &transcript(something)
1296 #    nochangebug
1297 #
1298 #    setbug (returns 1)
1299 #    $action= (something)
1300 #    do {
1301 #      (modify s_* variables)
1302 #    } while (getnextbug);
1303
1304 our $manybugs;
1305
1306 sub nochangebug {
1307     &dlen("nochangebug");
1308     $state eq 'single' || $state eq 'multiple' || die "$state ?";
1309     &cancelbug;
1310     &endmerge if $manybugs;
1311     $state= 'idle';
1312     &dlex("nochangebug");
1313 }
1314
1315 our $sref;
1316 our @thisbugmergelist;
1317
1318 sub setbug {
1319     &dlen("setbug $ref");
1320     if ($ref =~ m/^-\d+/) {
1321         if (!defined $clonebugs{$ref}) {
1322             &notfoundbug;
1323             &dlex("setbug => noclone");
1324             return 0;
1325         }
1326         $ref = $clonebugs{$ref};
1327     }
1328     $state eq 'idle' || die "$state ?";
1329     if (!&getbug) {
1330         &notfoundbug;
1331         &dlex("setbug => 0s");
1332         return 0;
1333     }
1334
1335     if (!&checkpkglimit) {
1336         &cancelbug;
1337         return 0;
1338     }
1339
1340     @thisbugmergelist= split(/ /,$data->{mergedwith});
1341     if (!@thisbugmergelist) {
1342         &foundbug;
1343         $manybugs= 0;
1344         $state= 'single';
1345         $sref=$ref;
1346         &dlex("setbug => 1s");
1347         return 1;
1348     }
1349     &cancelbug;
1350     &getmerge;
1351     $manybugs= 1;
1352     if (!&getbug) {
1353         &notfoundbug;
1354         &endmerge;
1355         &dlex("setbug => 0mc");
1356         return 0;
1357     }
1358     &foundbug;
1359     $state= 'multiple'; $sref=$ref;
1360     &dlex("setbug => 1m");
1361     return 1;
1362 }
1363
1364 sub getnextbug {
1365     &dlen("getnextbug");
1366     $state eq 'single' || $state eq 'multiple' || die "$state ?";
1367     &savebug;
1368     if (!$manybugs || !@thisbugmergelist) {
1369         length($action) || die;
1370         print {$transcript} "$action\n$extramessage\n";
1371         &endmerge if $manybugs;
1372         $state= 'idle';
1373         &dlex("getnextbug => 0");
1374         return 0;
1375     }
1376     $ref= shift(@thisbugmergelist);
1377     &getbug || die "bug $ref disappeared";
1378     &foundbug;
1379     &dlex("getnextbug => 1");
1380     return 1;
1381 }
1382
1383 # Low-level bug-manipulation calls
1384 # Do no announcements
1385 #
1386 #    getbug (returns 0)
1387 #
1388 #    getbug (returns 1)
1389 #    cancelbug
1390 #
1391 #    getmerge
1392 #    $action= (something)
1393 #    getbug (returns 1)
1394 #    savebug/cancelbug
1395 #    getbug (returns 1)
1396 #    savebug/cancelbug
1397 #    [getbug (returns 0)]
1398 #    &transcript("$action\n\n")
1399 #    endmerge
1400
1401 sub notfoundbug { print {$transcript} "$gBug number $ref not found. (Is it archived?)\n\n"; }
1402 sub foundbug { print {$transcript} "$gBug#$ref: $data->{subject}\n"; }
1403
1404 sub getmerge {
1405     &dlen("getmerge");
1406     $mergelowstate eq 'idle' || die "$mergelowstate ?";
1407     &filelock('lock/merge');
1408     $mergelowstate='locked';
1409     &dlex("getmerge");
1410 }
1411
1412 sub endmerge {
1413     &dlen("endmerge");
1414     $mergelowstate eq 'locked' || die "$mergelowstate ?";
1415     &unfilelock;
1416     $mergelowstate='idle';
1417     &dlex("endmerge");
1418 }
1419
1420 sub getbug {
1421     &dlen("getbug $ref");
1422     $lowstate eq 'idle' || die "$state ?";
1423     # Only use unmerged bugs here
1424     if (($data = &lockreadbug($ref,'db-h'))) {
1425         $sref= $ref;
1426         $lowstate= "open";
1427         &dlex("getbug => 1");
1428         $extramessage='';
1429         return 1;
1430     }
1431     $lowstate= 'idle';
1432     &dlex("getbug => 0");
1433     return 0;
1434 }
1435
1436 sub cancelbug {
1437     &dlen("cancelbug");
1438     $lowstate eq 'open' || die "$state ?";
1439     &unfilelock;
1440     $lowstate= 'idle';
1441     &dlex("cancelbug");
1442 }
1443
1444 sub savebug {
1445     &dlen("savebug $ref");
1446     $lowstate eq 'open' || die "$lowstate ?";
1447     length($action) || die;
1448     $ref == $sref || die "read $sref but saving $ref ?";
1449     append_action_to_log(bug => $ref,
1450                          action => $action,
1451                          requester => $header{from},
1452                          request_addr => $controlrequestaddr,
1453                          message => \@log,
1454                          get_lock => 0,
1455                         );
1456     unlockwritebug($ref, $data);
1457     $lowstate= "idle";
1458     &dlex("savebug");
1459 }
1460
1461 sub dlen {
1462     return if !$dl;
1463     print {$transcript} "C> @_ ($state $lowstate $mergelowstate)\n";
1464 }
1465
1466 sub dlex {
1467     return if !$dl;
1468     print {$transcript} "R> @_ ($state $lowstate $mergelowstate)\n";
1469 }
1470
1471 sub urlsanit {
1472     my $url = shift;
1473     $url =~ s/%/%25/g;
1474     $url =~ s/\+/%2b/g;
1475     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
1476     $url =~ s/([<>&"])/\&$saniarray{$1};/g;
1477     return $url;
1478 }
1479
1480 sub sendlynxdoc {
1481     &sendlynxdocraw;
1482     print {$transcript} "\n";
1483     $ok++;
1484 }
1485
1486 sub sendtxthelp {
1487     &sendtxthelpraw;
1488     print {$transcript} "\n";
1489     $ok++;
1490 }
1491
1492
1493 our $doc;
1494 sub sendtxthelpraw {
1495     my ($relpath,$description) = @_;
1496     $doc='';
1497     if (not -e "$gDocDir/$relpath") {
1498         print {$transcript} "Unfortunatly, the help text doesn't exist, so it wasn't sent.\n";
1499         warn "Help text $gDocDir/$relpath not found";
1500         return;
1501     }
1502     open(D,"$gDocDir/$relpath") || die "open doc file $relpath: $!";
1503     while(<D>) { $doc.=$_; }
1504     close(D);
1505     print {$transcript} "Sending $description in separate message.\n";
1506     &sendmailmessage(<<END.$doc,$replyto);
1507 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1508 To: $replyto
1509 Subject: $gProject $gBug help: $description
1510 References: $header{'message-id'}
1511 In-Reply-To: $header{'message-id'}
1512 Message-ID: <handler.s.$nn.help.$midix\@$gEmailDomain>
1513 Precedence: bulk
1514 X-$gProject-PR-Message: doc-text $relpath
1515
1516 END
1517     $ok++;
1518 }
1519
1520 sub sendlynxdocraw {
1521     my ($relpath,$description) = @_;
1522     $doc='';
1523     open(L,"lynx -nolist -dump http://$gCGIDomain/\Q$relpath\E 2>&1 |") || die "fork for lynx: $!";
1524     while(<L>) { $doc.=$_; }
1525     $!=0; close(L);
1526     if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
1527         print {$transcript} "Information ($description) is not available -\n".
1528              "perhaps the $gBug does not exist or is not on the WWW yet.\n";
1529          $ok++;
1530     } elsif ($?) {
1531         print {$transcript} "Error getting $description (code $? $!):\n$doc\n";
1532     } else {
1533         print {$transcript} "Sending $description.\n";
1534         &sendmailmessage(<<END.$doc,$replyto);
1535 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1536 To: $replyto
1537 Subject: $gProject $gBugs information: $description
1538 References: $header{'message-id'}
1539 In-Reply-To: $header{'message-id'}
1540 Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
1541 Precedence: bulk
1542 X-$gProject-PR-Message: doc-html $relpath
1543
1544 END
1545          $ok++;
1546     }
1547 }
1548
1549
1550 sub sendinfo {
1551     my ($wherefrom,$path,$description) = @_;
1552     if ($wherefrom eq "ftp.d.o") {
1553       $doc = `lynx -nolist -dump http://ftp.debian.org/debian/indices/$path.gz 2>&1 | gunzip -cf` or die "fork for lynx/gunzip: $!";
1554       $! = 0;
1555       if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
1556           print {$transcript} "$description is not available.\n";
1557           $ok++; return;
1558       } elsif ($?) {
1559           print {$transcript} "Error getting $description (code $? $!):\n$doc\n";
1560           return;
1561       }
1562     } elsif ($wherefrom eq "local") {
1563       open P, "$path";
1564       $doc = do { local $/; <P> };
1565       close P;
1566     } else {
1567       print {$transcript} "internal errror: info files location unknown.\n";
1568       $ok++; return;
1569     }
1570     print {$transcript} "Sending $description.\n";
1571     &sendmailmessage(<<END.$doc,$replyto);
1572 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1573 To: $replyto
1574 Subject: $gProject $gBugs information: $description
1575 References: $header{'message-id'}
1576 In-Reply-To: $header{'message-id'}
1577 Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
1578 Precedence: bulk
1579 X-$gProject-PR-Message: getinfo
1580
1581 $description follows:
1582
1583 END
1584     $ok++;
1585     print {$transcript} "\n";
1586 }