]> git.donarmstrong.com Git - debbugs.git/blob - scripts/service
merge changes from dla source branch
[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} "Failed to forcibly merge $ref: ".cleanup_eval_fail($@,$debug)."\n";
887         }
888     } elsif (m/^clone\s+#?(\d+)\s+((-\d+\s+)*-\d+)\s*$/i) {
889         $ok++;
890
891         my $origref = $1;
892         my @newclonedids = split /\s+/, $2;
893         my $newbugsneeded = scalar(@newclonedids);
894
895         $ref = $origref;
896         if (exists $clonebugs{$ref}) {
897             $ref = $clonebugs{$ref};
898         }
899         $bug_affected{$ref} = 1;
900         eval {
901             my %new_clones;
902             clone_bug(@common_control_options,
903                       bug => $ref,
904                       new_bugs => \@newclonedids,
905                       new_clones => \%new_clones,
906                      );
907             %clonebugs = (%clonebugs,
908                           %new_clones);
909         };
910         if ($@) {
911             $errors++;
912             print {$transcript} "Failed to clone $ref: ".cleanup_eval_fail($@,$debug)."\n";
913         }
914     } elsif (m/^package\:?\s+(\S.*\S)?\s*$/i) {
915         $ok++;
916         my @pkgs = split /\s+/, $1;
917         if (scalar(@pkgs) > 0) {
918                 %limit_pkgs = map { ($_, 1) } @pkgs;
919                 $limit{package} = [@pkgs];
920                 print {$transcript} "Limiting to bugs with field 'package' containing at least one of ".join(', ',map {qq('$_')} @pkgs)."\n";
921                 print {$transcript} "Limit currently set to";
922                 for my $limit_field (keys %limit) {
923                     print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n";
924                 }
925                 print {$transcript} "\n";
926         } else {
927             %limit_pkgs = ();
928             $limit{package} = [];
929             print {$transcript} "Limit cleared.\n\n";
930         }
931     } elsif (m/^limit\:?\s+(\S.*\S)\s*$/) {
932         $ok++;
933         my ($field,@options) = split /\s+/, $1;
934         $field = lc($field);
935         if ($field =~ /^(?:clear|unset|blank)$/) {
936             %limit = ();
937             print {$transcript} "Limit cleared.\n\n";
938         }
939         elsif (exists $Debbugs::Status::fields{$field} or $field eq 'source') {
940             # %limit can actually contain regexes, but because they're
941             # not evaluated in Safe, DO NOT allow them through without
942             # fixing this.
943             $limit{$field} = [@options];
944             print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n";
945             print {$transcript} "Limit currently set to";
946             for my $limit_field (keys %limit) {
947                 print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n";
948             }
949             print {$transcript} "\n";
950         }
951         else {
952             print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n";
953             $errors++;
954             last;
955         }
956     } elsif (m/^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/i) {
957         $ok++;
958         $ref = $1;
959         my $add_remove = $2 || '';
960         my $packages = $3 || '';
961         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
962         eval {
963              affects(@common_control_options,
964                      bug => $ref,
965                      package     => [splitpackages($3)],
966                      ($add_remove eq '+'?(add => 1):()),
967                      ($add_remove eq '-'?(remove => 1):()),
968                     );
969         };
970         if ($@) {
971             $errors++;
972             print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n";
973         }
974
975     } elsif (m/^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/i) {
976         $ok++;
977         $ref = $1;
978         my $summary_msg = length($2)?$2:undef;
979         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
980         eval {
981             summary(@common_control_options,
982                     bug          => $ref,
983                     summary      => $summary_msg,
984                    );
985         };
986         if ($@) {
987             $errors++;
988             print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n";
989         }
990
991     } elsif (m/^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/i) {
992         $ok++;
993         $ref = $1;
994         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
995         my $newowner = $2;
996         if ($newowner eq '!') {
997             $newowner = $replyto;
998         }
999         eval {
1000             owner(@common_control_options,
1001                   bug          => $ref,
1002                   owner        => $newowner,
1003                  );
1004         };
1005         if ($@) {
1006             $errors++;
1007             print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n";
1008         }
1009     } elsif (m/^noowner\s+\#?(-?\d+)\s*$/i) {
1010         $ok++;
1011         $ref = $1;
1012         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1013         eval {
1014             owner(@common_control_options,
1015                   bug          => $ref,
1016                   owner        => undef,
1017                  );
1018         };
1019         if ($@) {
1020             $errors++;
1021             print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n";
1022         }
1023     } elsif (m/^unarchive\s+#?(\d+)$/i) {
1024          $ok++;
1025          $ref = $1;
1026          $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1027          eval {
1028               bug_unarchive(@common_control_options,
1029                             bug        => $ref,
1030                             recipients => \%recipients,
1031                            );
1032          };
1033          if ($@) {
1034               $errors++;
1035          }
1036     } elsif (m/^archive\s+#?(\d+)$/i) {
1037          $ok++;
1038          $ref = $1;
1039          $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
1040          eval {
1041               bug_archive(@common_control_options,
1042                           bug => $ref,
1043                           ignore_time => 1,
1044                           archive_unarchived => 0,
1045                          );
1046          };
1047          if ($@) {
1048               $errors++;
1049          }
1050     } else {
1051         print {$transcript} "Unknown command or malformed arguments to command.\n\n";
1052         $errors++;
1053         if (++$unknowns >= 5) {
1054             print {$transcript} "Too many unknown commands, stopping here.\n\n";
1055             last;
1056         }
1057     }
1058 }
1059 if ($procline>$#bodylines) {
1060     print {$transcript} ">\nEnd of message, stopping processing here.\n\n";
1061 }
1062 if (!$ok && !$quickabort) {
1063     $errors++;
1064     print {$transcript} "No commands successfully parsed; sending the help text(s).\n";
1065     &sendhelp;
1066     print {$transcript} "\n";
1067 }
1068
1069 my @maintccs = determine_recipients(recipients => \%recipients,
1070                                     address_only => 1,
1071                                     cc => 1,
1072                                    );
1073 my $maintccs = 'Cc: '.join(",\n    ",
1074                     determine_recipients(recipients => \%recipients,
1075                                          cc => 1,
1076                                         )
1077                    )."\n";
1078
1079 my $packagepr = '';
1080 $packagepr = "X-${gProject}-PR-Package: " . join(keys %affected_packages) . "\n" if keys %affected_packages;
1081
1082 # Add Bcc's to subscribed bugs
1083 # now handled by Debbugs::Recipients
1084 #push @bcc, map {"bugs=$_\@$gListDomain"} keys %bug_affected;
1085
1086 if (!defined $header{'subject'} || $header{'subject'} eq "") {
1087   $header{'subject'} = "your mail";
1088 }
1089
1090 # Error text here advertises how many errors there were
1091 my $error_text = $errors > 0 ? " (with $errors errors)":'';
1092
1093 my $reply= <<END;
1094 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1095 To: $replyto
1096 ${maintccs}Subject: Processed${error_text}: $header{'subject'}
1097 In-Reply-To: $header{'message-id'}
1098 END
1099 $reply .= <<END;
1100 References: $header{'message-id'}
1101 Message-ID: <handler.s.$nn.transcript\@$gEmailDomain>
1102 Precedence: bulk
1103 ${packagepr}X-$gProject-PR-Message: transcript
1104 END
1105
1106 $reply .= fill_template('mail/message_body',
1107                           {body => "${transcript_scalar}Please contact me if you need assistance."},
1108                         );
1109
1110 my $repliedshow= join(', ',$replyto,
1111                       determine_recipients(recipients => \%recipients,
1112                                            cc => 1,
1113                                            address_only => 1,
1114                                           )
1115                      );
1116 # -1 is the service.in log
1117 &filelock("lock/-1");
1118 open(AP,">>db-h/-1.log") || die "open db-h/-1.log: $!";
1119 print(AP
1120       "\2\n$repliedshow\n\5\n$reply\n\3\n".
1121       "\6\n".
1122       "<strong>Request received</strong> from <code>".
1123       html_escape($header{'from'})."</code>\n".
1124       "to <code>".html_escape($controlrequestaddr)."</code>\n".
1125       "\3\n".
1126       "\7\n",escape_log(@log),"\n\3\n") || die "writing db-h/-1.log: $!";
1127 close(AP) || die "open db-h/-1.log: $!";
1128 &unfilelock;
1129 utime(time,time,"db-h");
1130
1131 &sendmailmessage($reply,
1132                  exists $header{'x-debbugs-no-ack'}?():$replyto,
1133                  make_list(values %{{determine_recipients(recipients => \%recipients,
1134                                                           address_only => 1,
1135                                                          )}}
1136                           ),
1137                 );
1138
1139 unlink("incoming/P$nn") || die "unlinking incoming/P$nn: $!";
1140
1141 sub sendmailmessage {
1142     my ($message,@recips) = @_;
1143     $message = "X-Loop: $gMaintainerEmail\n" . $message;
1144     send_mail_message(message    => $message,
1145                       recipients => \@recips,
1146                      );
1147     $midix++;
1148 }
1149
1150 sub fill_template{
1151      my ($template,$extra_var) = @_;
1152      $extra_var ||={};
1153      my $variables = {config => \%config,
1154                       defined($ref)?(ref    => $ref):(),
1155                       defined($data)?(data  => $data):(),
1156                       refs => [map {exists $clonebugs{$_}?$clonebugs{$_}:$_} keys %bug_affected],
1157                       %{$extra_var},
1158                      };
1159      my $hole_var = {'&bugurl' =>
1160                      sub{"$_[0]: ".
1161                               'http://'.$config{cgi_domain}.'/'.
1162                                    Debbugs::CGI::bug_links(bug=>$_[0],
1163                                                            links_only => 1,
1164                                                           );
1165                     }
1166                     };
1167      return fill_in_template(template => $template,
1168                              variables => $variables,
1169                              hole_var  => $hole_var,
1170                             );
1171 }
1172
1173 =head2 message_body_template
1174
1175      message_body_template('mail/ack',{ref=>'foo'});
1176
1177 Creates a message body using a template
1178
1179 =cut
1180
1181 sub message_body_template{
1182      my ($template,$extra_var) = @_;
1183      $extra_var ||={};
1184      my $body = fill_template($template,$extra_var);
1185      return fill_template('mail/message_body',
1186                           {%{$extra_var},
1187                            body => $body,
1188                           },
1189                          );
1190 }
1191
1192 sub sendhelp {
1193      if ($control) {
1194           &sendtxthelpraw("bug-maint-mailcontrol.txt","instructions for control\@$gEmailDomain")
1195      }
1196      else {
1197           &sendtxthelpraw("bug-log-mailserver.txt","instructions for request\@$gEmailDomain");
1198      }
1199 }
1200
1201 #sub unimplemented {
1202 #    print {$transcript} "Sorry, command $_[0] not yet implemented.\n\n";
1203 #}
1204 our %checkmatch_values;
1205 sub checkmatch {
1206     my ($string,$mvarname,$svarvalue,@newmergelist) = @_;
1207     my ($mvarvalue);
1208     if (@newmergelist) {
1209         $mvarvalue = $checkmatch_values{$mvarname};
1210         print {$transcript} "D| checkmatch \`$string' /$mvarname/$mvarvalue/$svarvalue/\n"
1211             if $dl;
1212         $mismatch .=
1213             "Values for \`$string' don't match:\n".
1214             " #$newmergelist[0] has \`$mvarvalue';\n".
1215             " #$ref has \`$svarvalue'\n"
1216             if $mvarvalue ne $svarvalue;
1217     } else {
1218          print {$transcript} "D| setupmatch \`$string' /$mvarname/$svarvalue/\n"
1219               if $dl;
1220          $checkmatch_values{$mvarname} = $svarvalue;
1221     }
1222 }
1223
1224 sub checkpkglimit {
1225     if (keys %limit_pkgs and not defined $limit_pkgs{$data->{package}}) {
1226         print {$transcript} "$gBug number $ref belongs to package $data->{package}, skipping.\n\n";
1227         $errors++;
1228         return 0;
1229     }
1230     return 1;
1231 }
1232
1233 sub manipset {
1234     my $list = shift;
1235     my $elt = shift;
1236     my $add = shift;
1237
1238     my %h = map { $_ => 1 } split ' ', $list;
1239     if ($add) {
1240         $h{$elt}=1;
1241     }
1242     else {
1243         delete $h{$elt};
1244     }
1245     return join ' ', sort keys %h;
1246 }
1247
1248 # High-level bug manipulation calls
1249 # Do announcements themselves
1250 #
1251 # Possible calling sequences:
1252 #    setbug (returns 0)
1253 #    
1254 #    setbug (returns 1)
1255 #    &transcript(something)
1256 #    nochangebug
1257 #
1258 #    setbug (returns 1)
1259 #    $action= (something)
1260 #    do {
1261 #      (modify s_* variables)
1262 #    } while (getnextbug);
1263
1264 our $manybugs;
1265
1266 sub nochangebug {
1267     &dlen("nochangebug");
1268     $state eq 'single' || $state eq 'multiple' || die "$state ?";
1269     &cancelbug;
1270     &endmerge if $manybugs;
1271     $state= 'idle';
1272     &dlex("nochangebug");
1273 }
1274
1275 our $sref;
1276 our @thisbugmergelist;
1277
1278 sub setbug {
1279     &dlen("setbug $ref");
1280     if ($ref =~ m/^-\d+/) {
1281         if (!defined $clonebugs{$ref}) {
1282             &notfoundbug;
1283             &dlex("setbug => noclone");
1284             return 0;
1285         }
1286         $ref = $clonebugs{$ref};
1287     }
1288     $state eq 'idle' || die "$state ?";
1289     if (!&getbug) {
1290         &notfoundbug;
1291         &dlex("setbug => 0s");
1292         return 0;
1293     }
1294
1295     if (!&checkpkglimit) {
1296         &cancelbug;
1297         return 0;
1298     }
1299
1300     @thisbugmergelist= split(/ /,$data->{mergedwith});
1301     if (!@thisbugmergelist) {
1302         &foundbug;
1303         $manybugs= 0;
1304         $state= 'single';
1305         $sref=$ref;
1306         &dlex("setbug => 1s");
1307         return 1;
1308     }
1309     &cancelbug;
1310     &getmerge;
1311     $manybugs= 1;
1312     if (!&getbug) {
1313         &notfoundbug;
1314         &endmerge;
1315         &dlex("setbug => 0mc");
1316         return 0;
1317     }
1318     &foundbug;
1319     $state= 'multiple'; $sref=$ref;
1320     &dlex("setbug => 1m");
1321     return 1;
1322 }
1323
1324 sub getnextbug {
1325     &dlen("getnextbug");
1326     $state eq 'single' || $state eq 'multiple' || die "$state ?";
1327     &savebug;
1328     if (!$manybugs || !@thisbugmergelist) {
1329         length($action) || die;
1330         print {$transcript} "$action\n$extramessage\n";
1331         &endmerge if $manybugs;
1332         $state= 'idle';
1333         &dlex("getnextbug => 0");
1334         return 0;
1335     }
1336     $ref= shift(@thisbugmergelist);
1337     &getbug || die "bug $ref disappeared";
1338     &foundbug;
1339     &dlex("getnextbug => 1");
1340     return 1;
1341 }
1342
1343 # Low-level bug-manipulation calls
1344 # Do no announcements
1345 #
1346 #    getbug (returns 0)
1347 #
1348 #    getbug (returns 1)
1349 #    cancelbug
1350 #
1351 #    getmerge
1352 #    $action= (something)
1353 #    getbug (returns 1)
1354 #    savebug/cancelbug
1355 #    getbug (returns 1)
1356 #    savebug/cancelbug
1357 #    [getbug (returns 0)]
1358 #    &transcript("$action\n\n")
1359 #    endmerge
1360
1361 sub notfoundbug { print {$transcript} "$gBug number $ref not found. (Is it archived?)\n\n"; }
1362 sub foundbug { print {$transcript} "$gBug#$ref: $data->{subject}\n"; }
1363
1364 sub getmerge {
1365     &dlen("getmerge");
1366     $mergelowstate eq 'idle' || die "$mergelowstate ?";
1367     &filelock('lock/merge');
1368     $mergelowstate='locked';
1369     &dlex("getmerge");
1370 }
1371
1372 sub endmerge {
1373     &dlen("endmerge");
1374     $mergelowstate eq 'locked' || die "$mergelowstate ?";
1375     &unfilelock;
1376     $mergelowstate='idle';
1377     &dlex("endmerge");
1378 }
1379
1380 sub getbug {
1381     &dlen("getbug $ref");
1382     $lowstate eq 'idle' || die "$state ?";
1383     # Only use unmerged bugs here
1384     if (($data = &lockreadbug($ref,'db-h'))) {
1385         $sref= $ref;
1386         $lowstate= "open";
1387         &dlex("getbug => 1");
1388         $extramessage='';
1389         return 1;
1390     }
1391     $lowstate= 'idle';
1392     &dlex("getbug => 0");
1393     return 0;
1394 }
1395
1396 sub cancelbug {
1397     &dlen("cancelbug");
1398     $lowstate eq 'open' || die "$state ?";
1399     &unfilelock;
1400     $lowstate= 'idle';
1401     &dlex("cancelbug");
1402 }
1403
1404 sub savebug {
1405     &dlen("savebug $ref");
1406     $lowstate eq 'open' || die "$lowstate ?";
1407     length($action) || die;
1408     $ref == $sref || die "read $sref but saving $ref ?";
1409     append_action_to_log(bug => $ref,
1410                          action => $action,
1411                          requester => $header{from},
1412                          request_addr => $controlrequestaddr,
1413                          message => \@log,
1414                          get_lock => 0,
1415                         );
1416     unlockwritebug($ref, $data);
1417     $lowstate= "idle";
1418     &dlex("savebug");
1419 }
1420
1421 sub dlen {
1422     return if !$dl;
1423     print {$transcript} "C> @_ ($state $lowstate $mergelowstate)\n";
1424 }
1425
1426 sub dlex {
1427     return if !$dl;
1428     print {$transcript} "R> @_ ($state $lowstate $mergelowstate)\n";
1429 }
1430
1431 sub urlsanit {
1432     my $url = shift;
1433     $url =~ s/%/%25/g;
1434     $url =~ s/\+/%2b/g;
1435     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
1436     $url =~ s/([<>&"])/\&$saniarray{$1};/g;
1437     return $url;
1438 }
1439
1440 sub sendlynxdoc {
1441     &sendlynxdocraw;
1442     print {$transcript} "\n";
1443     $ok++;
1444 }
1445
1446 sub sendtxthelp {
1447     &sendtxthelpraw;
1448     print {$transcript} "\n";
1449     $ok++;
1450 }
1451
1452
1453 our $doc;
1454 sub sendtxthelpraw {
1455     my ($relpath,$description) = @_;
1456     $doc='';
1457     if (not -e "$gDocDir/$relpath") {
1458         print {$transcript} "Unfortunatly, the help text doesn't exist, so it wasn't sent.\n";
1459         warn "Help text $gDocDir/$relpath not found";
1460         return;
1461     }
1462     open(D,"$gDocDir/$relpath") || die "open doc file $relpath: $!";
1463     while(<D>) { $doc.=$_; }
1464     close(D);
1465     print {$transcript} "Sending $description in separate message.\n";
1466     &sendmailmessage(<<END.$doc,$replyto);
1467 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1468 To: $replyto
1469 Subject: $gProject $gBug help: $description
1470 References: $header{'message-id'}
1471 In-Reply-To: $header{'message-id'}
1472 Message-ID: <handler.s.$nn.help.$midix\@$gEmailDomain>
1473 Precedence: bulk
1474 X-$gProject-PR-Message: doc-text $relpath
1475
1476 END
1477     $ok++;
1478 }
1479
1480 sub sendlynxdocraw {
1481     my ($relpath,$description) = @_;
1482     $doc='';
1483     open(L,"lynx -nolist -dump http://$gCGIDomain/\Q$relpath\E 2>&1 |") || die "fork for lynx: $!";
1484     while(<L>) { $doc.=$_; }
1485     $!=0; close(L);
1486     if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
1487         print {$transcript} "Information ($description) is not available -\n".
1488              "perhaps the $gBug does not exist or is not on the WWW yet.\n";
1489          $ok++;
1490     } elsif ($?) {
1491         print {$transcript} "Error getting $description (code $? $!):\n$doc\n";
1492     } else {
1493         print {$transcript} "Sending $description.\n";
1494         &sendmailmessage(<<END.$doc,$replyto);
1495 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1496 To: $replyto
1497 Subject: $gProject $gBugs information: $description
1498 References: $header{'message-id'}
1499 In-Reply-To: $header{'message-id'}
1500 Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
1501 Precedence: bulk
1502 X-$gProject-PR-Message: doc-html $relpath
1503
1504 END
1505          $ok++;
1506     }
1507 }
1508
1509
1510 sub sendinfo {
1511     my ($wherefrom,$path,$description) = @_;
1512     if ($wherefrom eq "ftp.d.o") {
1513       $doc = `lynx -nolist -dump http://ftp.debian.org/debian/indices/$path.gz 2>&1 | gunzip -cf` or die "fork for lynx/gunzip: $!";
1514       $! = 0;
1515       if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
1516           print {$transcript} "$description is not available.\n";
1517           $ok++; return;
1518       } elsif ($?) {
1519           print {$transcript} "Error getting $description (code $? $!):\n$doc\n";
1520           return;
1521       }
1522     } elsif ($wherefrom eq "local") {
1523       open P, "$path";
1524       $doc = do { local $/; <P> };
1525       close P;
1526     } else {
1527       print {$transcript} "internal errror: info files location unknown.\n";
1528       $ok++; return;
1529     }
1530     print {$transcript} "Sending $description.\n";
1531     &sendmailmessage(<<END.$doc,$replyto);
1532 From: $gMaintainerEmail ($gProject $gBug Tracking System)
1533 To: $replyto
1534 Subject: $gProject $gBugs information: $description
1535 References: $header{'message-id'}
1536 In-Reply-To: $header{'message-id'}
1537 Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
1538 Precedence: bulk
1539 X-$gProject-PR-Message: getinfo
1540
1541 $description follows:
1542
1543 END
1544     $ok++;
1545     print {$transcript} "\n";
1546 }