]> git.donarmstrong.com Git - debbugs.git/commitdiff
Merge branch 'control.at.submit'
authorDon Armstrong <don@donarmstrong.com>
Sat, 14 Jul 2012 03:54:18 +0000 (20:54 -0700)
committerDon Armstrong <don@donarmstrong.com>
Sat, 14 Jul 2012 03:54:18 +0000 (20:54 -0700)
Debbugs/Control.pm
Debbugs/Control/Service.pm [new file with mode: 0644]
Debbugs/MIME.pm
scripts/process
scripts/service
t/14_control_at_submit.t [new file with mode: 0644]

index 6851656f49c6b4d4bde5c4c857cbb8584a2cde0a..c681916ab98f515fe784c08e51a88b65c8d75a4c 100644 (file)
@@ -99,6 +99,7 @@ BEGIN{
                     clone   => [qw(clone_bug)],
                     archive => [qw(bug_archive bug_unarchive),
                                ],
+                    limit   => [qw(check_limit)],
                     log     => [qw(append_action_to_log),
                                ],
                    );
@@ -2020,7 +2021,7 @@ sub set_merged {
     $new_locks += $n_locks;
     %data = %{$data};
     @data = values %data;
-    if (not __check_limit(data => [@data],
+    if (not check_limit(data => [@data],
                          exists $param{limit}?(limit => $param{limit}):(),
                          transcript => $transcript,
                         )) {
@@ -3618,7 +3619,7 @@ sub __begin_control {
            }
        }
     }
-    if (not __check_limit(data => \@data,
+    if (not check_limit(data => \@data,
                          exists $param{limit}?(limit => $param{limit}):(),
                          transcript => $transcript,
                         )) {
@@ -3685,9 +3686,9 @@ sub __end_control {
 }
 
 
-=head2 __check_limit
+=head2 check_limit
 
-     __check_limit(data => \@data, limit => $param{limit});
+     check_limit(data => \@data, limit => $param{limit});
 
 
 Checks to make sure that bugs match any limits; each entry of @data
@@ -3704,7 +3705,7 @@ limit to succeed.
 =cut
 
 
-sub __check_limit{
+sub check_limit{
     my %param = validate_with(params => \@_,
                              spec   => {data  => {type => ARRAYREF|SCALAR,
                                                  },
@@ -3798,7 +3799,9 @@ sub __message_body_template{
      my $hole_var = {'&bugurl' =>
                     sub{"$_[0]: ".
                             'http://'.$config{cgi_domain}.'/'.
-                                Debbugs::CGI::bug_url($_[0]);
+                                Debbugs::CGI::bug_links(bug => $_[0],
+                                                        links_only => 1,
+                                                       );
                     }
                    };
 
diff --git a/Debbugs/Control/Service.pm b/Debbugs/Control/Service.pm
new file mode 100644 (file)
index 0000000..239f653
--- /dev/null
@@ -0,0 +1,713 @@
+# This module is part of debbugs, and is released
+# under the terms of the GPL version 2, or any later
+# version at your option.
+# See the file README and COPYING for more information.
+#
+# [Other people have contributed to this file; their copyrights should
+# go here too.]
+# Copyright 2007,2008,2009 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Control::Service;
+
+=head1 NAME
+
+Debbugs::Control::Service -- Handles the modification parts of scripts/service by calling Debbugs::Control
+
+=head1 SYNOPSIS
+
+use Debbugs::Control::Service;
+
+
+=head1 DESCRIPTION
+
+This module contains the code to implement the grammar of control@. It
+is abstracted here so that it can be called from process at submit
+time.
+
+All of the public functions take the following options:
+
+=over
+
+=item debug -- scalar reference to which debbuging information is
+appended
+
+=item transcript -- scalar reference to which transcript information
+is appended
+
+=item affected_bugs -- hashref which is updated with bugs affected by
+this function
+
+
+=back
+
+Functions which should (probably) append to the .log file take the
+following options:
+
+=over
+
+=item requester -- Email address of the individual who requested the change
+
+=item request_addr -- Address to which the request was sent
+
+=item request_nn -- Name of queue file which caused this request
+
+=item request_msgid -- Message id of message which caused this request
+
+=item location -- Optional location; currently ignored but may be
+supported in the future for updating archived bugs upon archival
+
+=item message -- The original message which caused the action to be taken
+
+=item append_log -- Whether or not to append information to the log.
+
+=back
+
+B<append_log> (for most functions) is a special option. When set to
+false, no appending to the log is done at all. When it is not present,
+the above information is faked, and appended to the log file. When it
+is true, the above options must be present, and their values are used.
+
+
+=head1 GENERAL FUNCTIONS
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+
+BEGIN{
+     $VERSION = 1.00;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = (control => [qw(control_line valid_control)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(keys %EXPORT_TAGS);
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+use Debbugs::Config qw(:config);
+use Debbugs::Common qw(cleanup_eval_fail);
+use Debbugs::Control qw(:all);
+use Debbugs::Status qw(splitpackages);
+use Params::Validate qw(:types validate_with);
+use List::Util qw(first);
+
+my %control_grammar =
+    (close => qr/(?i)^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/,
+     reassign => qr/(?i)^reassign\s+\#?(-?\d+)\s+ # bug and command
+                   (?:(?:((?:src:|source:)?$config{package_name_re}) # new package
+                           (?:\s+((?:$config{package_name_re}\/)?
+                                   $config{package_version_re}))?)| # optional version
+                       ((?:src:|source:)?$config{package_name_re} # multiple package form
+                           (?:\s*\,\s*(?:src:|source:)?$config{package_name_re})+))
+                   \s*$/x,
+     reopen => qr/(?i)^reopen\s+\#?(-?\d+)(?:\s+([\=\!]|(?:\S.*\S)))?$/,
+     found => qr{^(?:(?i)found)\s+\#?(-?\d+)
+                (?:\s+((?:$config{package_name_re}\/)?
+                        $config{package_version_re}
+                        # allow for multiple packages
+                        (?:\s*,\s*(?:$config{package_name_re}\/)?
+                            $config{package_version_re})*)
+                )?$}x,
+     notfound => qr{^(?:(?i)notfound)\s+\#?(-?\d+)
+                   \s+((?:$config{package_name_re}\/)?
+                       $config{package_version_re}
+                       # allow for multiple packages
+                       (?:\s*,\s*(?:$config{package_name_re}\/)?
+                           $config{package_version_re})*
+                   )$}x,
+     fixed => qr{^(?:(?i)fixed)\s+\#?(-?\d+)
+            \s+((?:$config{package_name_re}\/)?
+                   $config{package_version_re}
+               # allow for multiple packages
+               (?:\s*,\s*(?:$config{package_name_re}\/)?
+                   $config{package_version_re})*)
+           \s*$}x,
+     notfixed => qr{^(?:(?i)notfixed)\s+\#?(-?\d+)
+            \s+((?:$config{package_name_re}\/)?
+                   $config{package_version_re}
+               # allow for multiple packages
+               (?:\s*,\s*(?:$config{package_name_re}\/)?
+                   $config{package_version_re})*)
+           \s*$}x,
+     submitter => qr/(?i)^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/,
+     forwarded => qr/(?i)^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/,
+     notforwarded => qr/(?i)^notforwarded\s+\#?(-?\d+)$/,
+     severity => qr/(?i)^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/,
+     tag => qr/(?i)^tags?\s+\#?(-?\d+)\s+(\S.*)$/,
+     block => qr/(?i)^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/,
+     retitle => qr/(?i)^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/,
+     unmerge => qr/(?i)^unmerge\s+\#?(-?\d+)$/,
+     merge   => qr/(?i)^merge\s+#?(-?\d+(\s+#?-?\d+)+)\s*$/,
+     forcemerge => qr/(?i)^forcemerge\s+\#?(-?\d+(?:\s+\#?-?\d+)+)\s*$/,
+     clone => qr/(?i)^clone\s+#?(\d+)\s+((-\d+\s+)*-\d+)\s*$/,
+     package => qr/(?i)^package\:?\s+(\S.*\S)?\s*$/,
+     limit => qr/(?i)^limit\:?\s+(\S.*\S)\s*$/,
+     affects => qr/(?i)^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/,
+     summary => qr/(?i)^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/,
+     owner => qr/(?i)^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/,
+     noowner => qr/(?i)^noowner\s+\#?(-?\d+)\s*$/,
+     unarchive => qr/(?i)^unarchive\s+#?(\d+)$/,
+     archive => qr/(?i)^archive\s+#?(\d+)$/,
+    );
+
+sub valid_control {
+    my ($line,$matches) = @_;
+    my @matches;
+    for my $ctl (keys %control_grammar) {
+       if (@matches = $line =~ $control_grammar{$ctl}) {
+           @{$matches} = @matches if defined $matches and ref($matches) eq 'ARRAY';
+           return $ctl;
+       }
+    }
+    @{$matches} = () if defined $matches and ref($matches) eq 'ARRAY';
+    return undef;
+}
+
+sub control_line {
+    my %param =
+       validate_with(params => \@_,
+                     spec => {line => {type => SCALAR,
+                                      },
+                              clonebugs => {type => HASHREF,
+                                           },
+                              common_control_options => {type => ARRAYREF,
+                                                        },
+                              errors => {type => SCALARREF,
+                                        },
+                              transcript => {type => HANDLE,
+                                            },
+                              debug => {type => SCALAR,
+                                        default => 0,
+                                       },
+                              ok => {type => SCALARREF,
+                                    },
+                              limit => {type => HASHREF,
+                                       },
+                             },
+                    );
+    my $line = $param{line};
+    my @matches;
+    my $ctl = valid_control($line,\@matches);
+    my $transcript = $param{transcript};
+    my $debug = $param{debug};
+    if (not defined $ctl) {
+       ${$param{errors}}++;
+       print {$param{transcript}} "Unknown command or invalid options to control\n";
+       return;
+    }
+    # in almost all cases, the first match is the bug; the exception
+    # to this is block.
+    my $ref = $matches[0];
+    if (defined $ref) {
+       $ref = $param{clonebugs}{$ref} if exists $param{clonebugs}{$ref};
+    }
+    ${$param{ok}}++;
+    my $errors = 0;
+    my $terminate_control = 0;
+
+    if ($ctl eq 'close') {
+       if (defined $matches[1]) {
+           eval {
+               set_fixed(@{$param{common_control_options}},
+                         bug   => $ref,
+                         fixed => $matches[1],
+                         add   => 1,
+                        );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to add fixed version '$matches[1]' to $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+       eval {
+           set_done(@{$param{common_control_options}},
+                    done      => 1,
+                    bug       => $ref,
+                    reopen    => 0,
+                    notify_submitter => 1,
+                    clear_fixed => 0,
+                   );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref as done: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'reassign') {
+       my @new_packages;
+       if (not defined $matches[1]) {
+           push @new_packages, split /\s*\,\s*/,$matches[3];
+       }
+       else {
+           push @new_packages, $matches[1];
+       }
+       @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages;
+        my $version= $matches[2];
+       eval {
+           set_package(@{$param{common_control_options}},
+                       bug          => $ref,
+                       package      => \@new_packages,
+                      );
+           # if there is a version passed, we make an internal call
+           # to set_found
+           if (defined($version) && length $version) {
+               set_found(@{$param{common_control_options}},
+                         bug   => $ref,
+                         found => $version,
+                        );
+           }
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'reopen') {
+       my $new_submitter = $matches[1];
+       if (defined $new_submitter) {
+           if ($new_submitter eq '=') {
+               undef $new_submitter;
+           }
+           elsif ($new_submitter eq '!') {
+               $new_submitter = $param{replyto};
+           }
+       }
+       eval {
+           set_done(@{$param{common_control_options}},
+                    bug          => $ref,
+                    reopen       => 1,
+                    defined $new_submitter? (submitter    => $new_submitter):(),
+                   );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to reopen $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'found') {
+       my @versions;
+        if (defined $matches[1]) {
+           @versions = split /\s*,\s*/,$matches[1];
+           eval {
+               set_found(@{$param{common_control_options}},
+                         bug          => $ref,
+                         found        => \@versions,
+                         add          => 1,
+                        );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to add found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+       else {
+           eval {
+               set_fixed(@{$param{common_control_options}},
+                         bug          => $ref,
+                         fixed        => [],
+                         reopen       => 1,
+                        );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+    }
+    elsif ($ctl eq 'notfound') {
+       my @versions;
+        @versions = split /\s*,\s*/,$matches[1];
+       eval {
+           set_found(@{$param{common_control_options}},
+                     bug          => $ref,
+                     found        => \@versions,
+                     remove       => 1,
+                    );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to remove found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    }
+    elsif ($ctl eq 'fixed') {
+       my @versions;
+        @versions = split /\s*,\s*/,$matches[1];
+       eval {
+           set_fixed(@{$param{common_control_options}},
+                     bug          => $ref,
+                     fixed        => \@versions,
+                     add          => 1,
+                    );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to add fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    }
+    elsif ($ctl eq 'notfixed') {
+       my @versions;
+        @versions = split /\s*,\s*/,$matches[1];
+       eval {
+           set_fixed(@{$param{common_control_options}},
+                     bug          => $ref,
+                     fixed        => \@versions,
+                     remove       => 1,
+                    );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to remove fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    }
+    elsif ($ctl eq 'submitter') {
+       my $newsubmitter = $matches[1] eq '!' ? $param{replyto} : $matches[1];
+        if (not Mail::RFC822::Address::valid($newsubmitter)) {
+            print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n";
+            $errors++;
+       }
+       else {
+           eval {
+               set_submitter(@{$param{common_control_options}},
+                             bug       => $ref,
+                             submitter => $newsubmitter,
+                            );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to set submitter on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+        }
+    } elsif ($ctl eq 'forwarded') {
+       my $forward_to= $matches[1];
+       eval {
+           set_forwarded(@{$param{common_control_options}},
+                         bug          => $ref,
+                         forwarded    => $forward_to,
+                          );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to set the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'notforwarded') {
+       eval {
+           set_forwarded(@{$param{common_control_options}},
+                         bug          => $ref,
+                         forwarded    => undef,
+                          );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to clear the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'severity') {
+       my $newseverity= $matches[1];
+        if (exists $config{obsolete_severities}{$newseverity}) {
+            print {$transcript} "Severity level \`$newseverity' is obsolete. " .
+                "Use $config{obsolete_severities}{$newseverity} instead.\n\n";
+               $errors++;
+        } elsif (not defined first {$_ eq $newseverity}
+           (@{$config{severity_list}}, $config{default_severity})) {
+            print {$transcript} "Severity level \`$newseverity' is not known.\n".
+                 "Recognized are: $config{show_severities}.\n\n";
+           $errors++;
+        } else {
+           eval {
+               set_severity(@{$param{common_control_options}},
+                            bug => $ref,
+                            severity => $newseverity,
+                           );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to set severity of $config{bug} $ref to $newseverity: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+    } elsif ($ctl eq 'tag') {
+       my $tags = $matches[1];
+       my @tags = map {m/^([+=-])(.+)/ ? ($1,$2):($_)} split /[\s,]+/, $tags;
+       # this is an array of hashrefs which contain two elements, the
+       # first of which is the array of tags, the second is the
+       # option to pass to set_tag (we use a hashref here to make it
+       # more obvious what is happening)
+       my @tag_operations;
+       my @badtags;
+       for my $tag (@tags) {
+           if ($tag =~ /^[=+-]$/) {
+               if ($tag eq '=') {
+                   @tag_operations = {tags => [],
+                                      option => [],
+                                     };
+               }
+               elsif ($tag eq '-') {
+                   push @tag_operations,
+                       {tags => [],
+                        option => [remove => 1],
+                       };
+               }
+               elsif ($tag eq '+') {
+                   push @tag_operations,
+                       {tags => [],
+                        option => [add => 1],
+                       };
+               }
+               next;
+           }
+           if (not defined first {$_ eq $tag} @{$config{tags}}) {
+               push @badtags, $tag;
+               next;
+           }
+           if (not @tag_operations) {
+               @tag_operations = {tags => [],
+                                  option => [add => 1],
+                                 };
+           }
+           push @{$tag_operations[-1]{tags}},$tag;
+       }
+       if (@badtags) {
+            print {$transcript} "Unknown tag/s: ".join(', ', @badtags).".\n".
+                "Recognized are: ".join(' ', @{$config{tags}}).".\n\n";
+           $errors++;
+       }
+       eval {
+           for my $operation (@tag_operations) {
+               set_tag(@{$param{common_control_options}},
+                       bug => $ref,
+                       tag => [@{$operation->{tags}}],
+                       warn_on_bad_tags => 0, # don't warn on bad tags,
+                       # 'cause we do that above
+                       @{$operation->{option}},
+                      );
+           }
+       };
+       if ($@) {
+           # we intentionally have two errors here if there is a bad
+           # tag and the above fails for some reason
+           $errors++;
+           print {$transcript} "Failed to alter tags of $config{bug} $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'block') {
+       my $add_remove = defined $matches[0] && $matches[0] eq 'un';
+       $ref = $matches[1];
+       $ref = exists $param{clonebugs}{$ref} ? $param{clonebugs}{$ref} : $ref;
+       my @blockers = map {exists $param{clonebugs}{$_}?$param{clonebugs}{$_}:$_} split /[\s,]+/, $matches[2];
+       eval {
+            set_blocks(@{$param{common_control_options}},
+                       bug          => $ref,
+                       block        => \@blockers,
+                       $add_remove ? (remove => 1):(add => 1),
+                      );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to set blocking bugs of $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'retitle') {
+        my $newtitle= $matches[1];
+       eval {
+            set_title(@{$param{common_control_options}},
+                      bug          => $ref,
+                      title        => $newtitle,
+                     );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to set the title of $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'unmerge') {
+       eval {
+            set_merged(@{$param{common_control_options}},
+                       bug          => $ref,
+                      );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to unmerge $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'merge') {
+       my @tomerge;
+        ($ref,@tomerge) = map {exists $param{clonebugs}{$_}?$param{clonebugs}{$_}:$_}
+           split(/\s+#?/,$matches[0]);
+       eval {
+            set_merged(@{$param{common_control_options}},
+                       bug          => $ref,
+                       merge_with   => \@tomerge,
+                      );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to merge $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'forcemerge') {
+       my @tomerge;
+        ($ref,@tomerge) = map {exists $param{clonebugs}{$_}?$param{clonebugs}{$_}:$_}
+           split(/\s+#?/,$matches[0]);
+       eval {
+            set_merged(@{$param{common_control_options}},
+                       bug          => $ref,
+                       merge_with   => \@tomerge,
+                       force        => 1,
+                       masterbug    => 1,
+                      );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to forcibly merge $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'clone') {
+       my $origref = $matches[0];
+       my @newclonedids = split /\s+/, $matches[1];
+       my $newbugsneeded = scalar(@newclonedids);
+
+       eval {
+           my %new_clones;
+           clone_bug(@{$param{common_control_options}},
+                     bug => $ref,
+                     new_bugs => \@newclonedids,
+                     new_clones => \%new_clones,
+                    );
+           %{$param{clonebugs}} = (%{$param{clonebugs}},
+                                   %new_clones);
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to clone $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'package') {
+       my @pkgs = split /\s+/, $matches[0];
+       if (scalar(@pkgs) > 0) {
+               $param{limit}{package} = [@pkgs];
+               print {$transcript} "Limiting to bugs with field 'package' containing at least one of ".join(', ',map {qq('$_')} @pkgs)."\n";
+               print {$transcript} "Limit currently set to";
+               for my $limit_field (keys %{$param{limit}}) {
+                   print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$param{limit}{$limit_field}})."\n";
+               }
+               print {$transcript} "\n";
+       } else {
+           $param{limit}{package} = [];
+           print {$transcript} "Limit cleared.\n\n";
+       }
+    } elsif ($ctl eq 'limit') {
+       my ($field,@options) = split /\s+/, $matches[0];
+       $field = lc($field);
+       if ($field =~ /^(?:clear|unset|blank)$/) {
+           %{$param{limit}} = ();
+           print {$transcript} "Limit cleared.\n\n";
+       }
+       elsif (exists $Debbugs::Status::fields{$field} or $field eq 'source') {
+           # %{$param{limit}} can actually contain regexes, but because they're
+           # not evaluated in Safe, DO NOT allow them through without
+           # fixing this.
+           $param{limit}{$field} = [@options];
+           print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n";
+           print {$transcript} "Limit currently set to";
+           for my $limit_field (keys %{$param{limit}}) {
+               print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$param{limit}{$limit_field}})."\n";
+           }
+           print {$transcript} "\n";
+       }
+       else {
+           print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n";
+           $errors++;
+           # this needs to be fixed
+           syntax error for fixing it
+           last;
+       }
+    } elsif ($ctl eq 'affects') {
+       my $add_remove = $matches[1];
+       my $packages = $matches[2];
+       # if there isn't a package given, assume that we should unset
+       # affects; otherwise default to adding
+       if (not defined $packages or
+           not length $packages) {
+           $packages = '';
+           $add_remove ||= '=';
+       }
+       elsif (not defined $add_remove or
+              not length $add_remove) {
+           $add_remove = '+';
+       }
+       eval {
+            affects(@{$param{common_control_options}},
+                    bug => $ref,
+                    package     => [splitpackages($packages)],
+                    ($add_remove eq '+'?(add => 1):()),
+                    ($add_remove eq '-'?(remove => 1):()),
+                   );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n";
+       }
+
+    } elsif ($ctl eq 'summary') {
+       my $summary_msg = length($matches[1])?$matches[1]:undef;
+       eval {
+           summary(@{$param{common_control_options}},
+                   bug          => $ref,
+                   summary      => $summary_msg,
+                  );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+
+    } elsif ($ctl eq 'owner') {
+       my $newowner = $matches[1];
+       if ($newowner eq '!') {
+           $newowner = $param{replyto};
+       }
+       eval {
+           owner(@{$param{common_control_options}},
+                 bug          => $ref,
+                 owner        => $newowner,
+                );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'noowner') {
+       eval {
+           owner(@{$param{common_control_options}},
+                 bug          => $ref,
+                 owner        => undef,
+                );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif ($ctl eq 'unarchive') {
+        eval {
+             bug_unarchive(@{$param{common_control_options}},
+                           bug        => $ref,
+                          );
+        };
+        if ($@) {
+             $errors++;
+        }
+    } elsif ($ctl eq 'archive') {
+        eval {
+             bug_archive(@{$param{common_control_options}},
+                         bug => $ref,
+                         ignore_time => 1,
+                         archive_unarchived => 0,
+                        );
+        };
+        if ($@) {
+             $errors++;
+        }
+    }
+    if ($errors) {
+       ${$param{errors}}+=$errors;
+    }
+    return($errors,$terminate_control);
+}
+
+1;
+
+__END__
index 05534e3e054c47d2cb281f683aaacf1a0ac7a52e..481be7bf9573f758ff331292b8f5a61fe66b75a6 100644 (file)
@@ -31,12 +31,21 @@ use warnings;
 use strict;
 
 use base qw(Exporter);
-use vars qw($VERSION @EXPORT_OK);
+use vars qw($DEBUG $VERSION @EXPORT_OK %EXPORT_TAGS @EXPORT);
 
 BEGIN {
     $VERSION = 1.00;
+    $DEBUG = 0 unless defined $DEBUG;
 
-    @EXPORT_OK = qw(parse decode_rfc1522 encode_rfc1522 convert_to_utf8 create_mime_message getmailbody);
+    @EXPORT = ();
+
+    %EXPORT_TAGS = (mime => [qw(parse create_mime_message getmailbody)],
+                   rfc1522 => [qw(decode_rfc1522 encode_rfc1522)],
+                   utf8 => [qw(convert_to_utf8)],
+                  );
+    @EXPORT_OK=();
+    Exporter::export_ok_tags(keys %EXPORT_TAGS);
+    $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
 use File::Path;
index 5687bf3915e522d90736f9b55c8944c7933cfad6..4236f6b4ccbdece729d1f8488c8cc0bf4afc204e 100755 (executable)
@@ -11,15 +11,17 @@ use POSIX qw(strftime);
 
 use IO::File;
 
+use Getopt::Long;
+use Pod::Usage;
 use MIME::Parser;
 use Debbugs::MIME qw(decode_rfc1522 create_mime_message getmailbody);
 use Debbugs::Mail qw(send_mail_message encode_headers get_addresses);
 use Debbugs::Packages qw(getpkgsrc binary_to_source);
 use Debbugs::User qw(read_usertags write_usertags);
-use Debbugs::Common qw(:lock get_hashname package_maintainer overwritefile);
+use Debbugs::Common qw(:lock get_hashname buglog package_maintainer overwritefile make_list);
 use Debbugs::Status qw(writebug isstrongseverity lockreadbugmerge lockreadbug new_bug read_bug splitpackages  :versions);
 
-use Debbugs::CGI qw(html_escape bug_url);
+use Debbugs::CGI qw(html_escape bug_links);
 
 use Debbugs::Log qw(:misc :write);
 
@@ -28,53 +30,119 @@ use Debbugs::Text qw(:templates);
 use Debbugs::Config qw(:globals :config);
 
 use Debbugs::Control qw(append_action_to_log);
-use Encode qw(encode_utf8);
+use Debbugs::Control::Service qw(valid_control control_line);
+use Debbugs::Recipients qw(determine_recipients);
+use Encode qw(encode_utf8 decode);
 
-chdir( "$gSpoolDir" ) || die "chdir spool: $!\n";
+=head1 NAME
+
+process - Handle e-mails emails sent to bugs
+
+=head1 SYNOPSIS
+
+process nn
+
+ Options:
+  --debug, -d debugging level (Default 0)
+
+=head1 OPTIONS
+
+=over
+
+=item <--debug,-d>
+
+Debugging level (default 0)
+
+=back
+
+=cut
+
+use vars qw($DEBUG);
+
+my %options = (debug           => 0,
+              help            => 0,
+              man             => 0,
+             );
+
+GetOptions(\%options,
+          'debug|d+','help|h|?','man|m');
+
+pod2usage() if $options{help};
+pod2usage({verbose=>2}) if $options{man};
+
+
+$DEBUG=$options{debug};
+my $debugfh = IO::File->new('/dev/null','w') or
+    die "Unable to open /dev/null for writing; $!";
+if ($DEBUG > 0) {
+    $debugfh = \*STDERR;
+}
+
+# these are the valid bug addresses
+my %baddress = (B => 'submit',
+               M => 'maintonly',
+               Q => 'quiet',
+               F => 'forwarded',
+               D => 'done',
+               S => 'submitter',
+               L => 'list',
+              );
+my $valid_codeletters = join('',keys %baddress);
+
+
+chdir($config{spool_dir}) or die "Unable to chdir to spool ($config{spool_dir}): $!";
 
-#open(DEBUG,"> /tmp/debbugs.debug");
 umask(002);
-open DEBUG, ">/dev/null";
 
 my $intdate = time or die "failed to get time: $!";
 
-$_=shift;
-m/^([BMQFDUL])(\d*)\.\d+$/ or die "bad argument: $_";
-my $codeletter= $1;
-my $tryref= length($2) ? $2 : -1;
-my $nn= $_;
+my ($nn) = @ARGV;
+my ($codeletter,$tryref) =
+    $nn =~ m/^([$valid_codeletters])(\d*)\.\d+$/
+    or die "bad argument: $_";
+$tryref = undef unless length ($tryref) and
+    $tryref > 0;
+
+if (!rename("incoming/G$nn","incoming/P$nn"))  {
+    my $error = $!;
+    $error = '' if not defined $error;
+    # this is very fragile, but we should probably die here anyway
+    if ($error =~ m/no such file or directory/i) {
+       exit 0;
+    }
+    die "Unable to rename incoming/G$nn to lock: $error";
+}
 
-if (!rename("incoming/G$nn","incoming/P$nn")) 
-{
-    $_=$!.'';  m/no such file or directory/i && exit 0;
-    die "renaming to lock: $!";
+# die here to avoid continuously processing this mail
+if (not exists $baddress{$codeletter}) {
+    die "bad codeletter $codeletter";
+}
+
+my $baddress = $baddress{$codeletter};
+if ($baddress eq 'list') {
+    bug_list_forward($nn) if $codeletter eq 'L';
 }
 
-my $baddress= 'submit' if $codeletter eq 'B';
-$baddress= 'maintonly' if $codeletter eq 'M';
-$baddress= 'quiet' if $codeletter eq 'Q';
-$baddress= 'forwarded' if $codeletter eq 'F';
-$baddress= 'done' if $codeletter eq 'D';
-$baddress= 'submitter' if $codeletter eq 'U';
-bug_list_forward($nn) if $codeletter eq 'L';
-$baddress || die "bad codeletter $codeletter";
+
 my $baddressroot= $baddress;
-$baddress= "$tryref-$baddress" if $tryref>=0;
+$baddress= "$tryref-$baddress" if defined $tryref;
 
-open(M,"incoming/P$nn");
-my @log=<M>;
-close(M);
+my $msg;
+my @msg;
 
-my @msg = @log;
-chomp @msg;
+{
+    my $log = IO::File->new("incoming/P$nn",'r') or
+       die "Unable to open 'incoming/P$nn' for reading; $!";
+    local $/;
+    $msg=<$log>;
+    @msg = split /\n/, $msg;
+    close($log);
+}
 
-print DEBUG "###\n",join("##\n",@msg),"\n###\n";
 
 my $tdate = strftime "%a, %d %h %Y %T +0000", gmtime;
-my $fwd= <<END;
-Received: via spool by $baddress\@$gEmailDomain id=$nn
-          (code $codeletter ref $tryref); $tdate
-END
+my $fwd= "Received: via spool by $baddress\@$gEmailDomain id=$nn\n".
+    "          (code $codeletter".(defined($tryref)?" ref $tryref":'')."); $tdate\n";
 
 # header and decoded body respectively
 my (@headerlines, @bodylines);
@@ -95,38 +163,10 @@ our $newref = 0;
 
 our $brokenness = '';
 
-my $parser = new MIME::Parser;
-mkdir "$gSpoolDir/mime.tmp", 0777;
-$parser->output_under("$gSpoolDir/mime.tmp");
-my $entity = eval { $parser->parse_data(join('',@log)) };
-
-my $i;
-if ($entity and $entity->head->tags) {
-    @headerlines = @{$entity->head->header};
-    chomp @headerlines;
-
-    my $entity_body = getmailbody($entity);
-    @bodylines = map {s/\r?\n$//; $_;}
-        $entity_body ? $entity_body->as_lines() : ();
-
-    # set $i to beginning of encoded body data, so we can dump it out
-    # verbatim later
-    $i = 0;
-    ++$i while $msg[$i] =~ /./;
-} else {
-    # Legacy pre-MIME code, kept around in case MIME::Parser fails.
-    for ($i = 0; $i <= $#msg; $i++) {
-       $_ = $msg[$i];
-       last unless length($_);
-       while ($msg[$i+1] =~ m/^\s/) {
-           $i++;
-           $_ .= "\n".$msg[$i];
-       }
-       push @headerlines, $_;
-    }
+my $parser_output = Debbugs::MIME::parse($msg);
 
-    @bodylines = @msg[$i..$#msg];
-}
+@headerlines = @{$parser_output->{header}};
+@bodylines = @{$parser_output->{body}};
 
 my %header;
 
@@ -135,20 +175,20 @@ for my $hdr (@headerlines) {
     $hdr = decode_rfc1522($hdr);
     $_ = $hdr;
     s/\n\s/ /g;
-    &finish if m/^x-loop: (\S+)$/i && $1 eq "$gMaintainerEmail";
+    finish() if m/^x-loop: (\S+)$/i && $1 eq "$gMaintainerEmail";
     my $ins = !m/^subject:/i && !m/^reply-to:/i && !m/^return-path:/i
            && !m/^From / && !m/^X-Debbugs-/i;
     $fwd .= $hdr."\n" if $ins;
-    # print DEBUG ">$_<\n";
+    # print {$debugfh} ">$_<\n";
     if (s/^(\S+):\s*//) {
        my $v = lc $1;
        if ($v eq 'x-loop') {
            push @common_headers, 'X-Loop',$_;
        }
-       print DEBUG ">$v=$_<\n";
+       print {$debugfh} ">$v=$_<\n";
        $header{$v} = $_;
     } else {
-       print DEBUG "!>$_<\n";
+       print {$debugfh} "!>$_<\n";
     }
 }
 $header{'message-id'} = '' if not defined $header{'message-id'};
@@ -173,6 +213,7 @@ if (@bodylines and $bodylines[0] =~ /^-----BEGIN PGP SIGNED/) {
 
 #psuedoheaders
 my %pheader;
+my @control_bits;
 # extract pseudo-headers
 for my $phline (@bodylines)
 {
@@ -182,12 +223,16 @@ for my $phline (@bodylines)
     last if $phline !~ m/^([\w-]+):\s*(\S.*)/;
     my ($fn, $fv) = ($1, $2);
     $fv =~ s/\s*$//;
-    print DEBUG ">$fn|$fv|\n";
+    print {$debugfh} ">$fn|$fv|\n";
     $fn = lc $fn;
-    # Don't lc owner or forwarded
-    $fv = lc $fv unless $fn =~ /^(?:owner|forwarded|usertags|version|source-version)$/;
-    $pheader{$fn} = $fv;
-    print DEBUG ">$fn~$fv<\n";
+    if ($fn =~ /^control$/) {
+       push @control_bits,$fv;
+    } else {
+       # Don't lc owner or forwarded
+       $fv = lc $fv unless $fn =~ /^(?:owner|forwarded|usertags|version|source-version)$/;
+       $pheader{$fn} = $fv;
+    }
+    print {$debugfh} ">$fn~$fv<\n";
 }
 
 # Allow pseudo headers to set x-debbugs- stuff [#179340]
@@ -195,9 +240,13 @@ for my $key (grep /X-Debbugs-.*/i, keys %pheader) {
      $header{$key} = $pheader{$key} if not exists $header{$key};
 }
 
+# set $i to beginning of encoded body data, so we can dump it out
+# verbatim later
+my $i = 0;
+++$i while $msg[$i] =~ /./;
 $fwd .= join("\n",@msg[$i..$#msg]);
 
-print DEBUG "***\n$fwd\n***\n";
+print {$debugfh} "***\n$fwd\n***\n";
 
 if (defined $header{'resent-from'} && !defined $header{'from'}) {
     $header{'from'} = $header{'resent-from'};
@@ -223,12 +272,11 @@ if (!defined($header{'subject'}))
 
 my $ref=-1;
 $subject =~ s/^Re:\s*//i; $_= $subject."\n";
-if ($tryref < 0 && m/^Bug ?\#(\d+)\D/i) {
-    $tryref= $1+0; 
+if (not defined $tryref and m/^Bug ?\#(\d+)\D/i) {
+    $tryref = $1 if $1 > 0;
 }
 my $data;
-if ($tryref >= 0) 
-{
+if (defined $tryref) {
      my $bfound;
     ($bfound, $data)= &lockreadbugmerge($tryref);
     if ($bfound and not $data->{archived}) {
@@ -252,8 +300,8 @@ if ($tryref >= 0)
                                   messageid => $header{'message-id'},
                                  },
                                 )),'');
-        &appendlog;
-        &finish;
+        appendlog($ref,$msg);
+        finish();
     }
 } else { 
     &filelock('lock/-1'); 
@@ -289,7 +337,7 @@ if ($codeletter eq 'D' || $codeletter eq 'F')
     my $generalcc;
     my $set_done;
     if ($codeletter eq 'F') { # Forwarded
-        (&appendlog,&finish) if defined $data->{forwarded} and length($data->{forwarded});
+        (appendlog($ref,$msg),finish()) if defined $data->{forwarded} and length($data->{forwarded});
         $receivedat= "forwarded\@$gEmailDomain";
         $markaswhat= 'forwarded';
         $set_forwarded= $header{'to'};
@@ -307,8 +355,8 @@ if ($codeletter eq 'D' || $codeletter eq 'F')
         if (defined $data->{done} and length($data->{done}) and
                 not defined $pheader{'source-version'} and
                 not defined $pheader{'version'}) {
-            &appendlog;
-            &finish;
+            appendlog($ref,$msg);
+            finish();
         }
         $receivedat= "done\@$gEmailDomain";
         $markaswhat= 'done';
@@ -343,8 +391,8 @@ if ($codeletter eq 'D' || $codeletter eq 'F')
                                   messageid => $header{'message-id'},
                                  },
                                 )),'');
-       &appendlog;
-       &finish;
+       appendlog($ref,$msg);
+       finish();
     }
 
     &checkmaintainers;
@@ -400,8 +448,12 @@ if ($codeletter eq 'D' || $codeletter eq 'F')
        writebug($ref, $data);
 
        my $hash = get_hashname($ref);
-        open(O,"db-h/$hash/$ref.report") || die "read original report: $!";
-        my $orig_report= join('',<O>); close(O);
+       my $orig_report_fh = IO::File->new("db-h/$hash/$ref.report") or
+           die "Unable to read original report: $!";
+       my $orig_report;
+       { local $/; $orig_report = <$orig_report_fh>;}
+       close($orig_report_fh) or
+           die "Unable to close original report filehandle: $!";
         if ($codeletter eq 'F') {
            &htmllog("Reply","sent",$replyto,"You have marked $gBug as forwarded.");
             &sendmessage(create_mime_message(
@@ -475,9 +527,9 @@ if ($codeletter eq 'D' || $codeletter eq 'F')
                                    ),
             [join("\n",@msg),$orig_report]),'',undef,1);
         }
-       &appendlog;
+       appendlog($ref,$msg);
     }
-    &finish;
+    finish();
 }
 
 if ($ref<0) { # new bug report
@@ -500,8 +552,8 @@ if ($ref<0) { # new bug report
                                   messageid => $header{'message-id'},
                                  },
                                 )),'');
-       &appendlog;
-       &finish;
+       appendlog($ref,$msg);
+       finish();
     }
 
     $data->{found_versions} = [];
@@ -539,8 +591,8 @@ if ($ref<0) { # new bug report
                                  baddress => $baddress,
                                 },
                                ),[join("\n", @msg)]), '',undef,1);
-       &appendlog;
-       &finish;
+       appendlog($ref,$msg);
+       finish();
     }
 
     if (defined $config{default_package}) {
@@ -634,7 +686,7 @@ if ($ref<0) { # new bug report
 
 &checkmaintainers;
 
-print DEBUG "maintainers >".join(' ',@maintaddrs)."<\n";
+print {$debugfh} "maintainers >".join(' ',@maintaddrs)."<\n";
 
 my $orgsender= defined($header{'sender'}) ? "Original-Sender: $header{'sender'}\n" : '';
 my $newsubject= $subject;  $newsubject =~ s/^$gBug#$ref:*\s*//;
@@ -903,18 +955,137 @@ if (not exists $header{'x-debbugs-no-ack'} and
                       ],$body,[]), '',undef,1);
 }
 
-&appendlog;
-&finish;
+appendlog($ref,$msg);
+
+## handle control messages at this point, immediately before finishing
+my %clonebugs = (-1 => $ref);
+my %bug_affected;
+if (@control_bits) {
+    my $transcript_scalar = '';
+    open my $transcript, ">:scalar:utf8", \$transcript_scalar or
+       die "Unable to create transcript scalar: $!";
+    print {$transcript} "Processing control commands:\n\n";
+    my %affected_packages;
+    my %recipients;
+    # this is the hashref which is passed to all control calls
+    my %limit = ();
+    my $errors = 0;
+    my $unknowns = 0;
+
+    my @common_control_options =
+       (transcript        => $transcript,
+        requester         => $header{from},
+        request_addr      => $baddress.'@'.$config{email_domain},
+        request_msgid     => $header{'message-id'},
+        request_subject   => $header{subject},
+        request_nn        => $nn,
+        request_replyto   => $replyto,
+        message           => $msg,
+        affected_bugs     => \%bug_affected,
+        affected_packages => \%affected_packages,
+        recipients        => \%recipients,
+        limit             => \%limit,
+       );
+    if (@gExcludeFromControl and grep {$replyto =~ m/\Q$_\E/} @gExcludeFromControl) {
+       print {$transcript} fill_template('mail/excluded_from_control');
+       print {$transcript} "Stopping processing here.\n\n";
+    } else {
+       my %clonebugs = ();
+       for my $control_bit (@control_bits) {
+           $control_bit =~ s/\xef\xbb\xbf//g;
+           next unless $control_bit =~ m/\S/;
+           eval {
+               my $temp = decode("utf8",$control_bit,Encode::FB_CROAK);
+               $control_bit = $temp;
+           };
+           print {$transcript} "> $control_bit\n";
+           next if $control_bit =~ /^\s*\#/;
+           my $action = '';
+           my $ok;
+           if (defined valid_control($control_bit)) {
+               my ($new_errors,$terminate_control) =
+                   control_line(line => $control_bit,
+                                clonebugs => \%clonebugs,
+                                limit => \%limit,
+                                common_control_options => \@common_control_options,
+                                errors => \$errors,
+                                transcript => $transcript,
+                                debug => 0,
+                                ok => \$ok,
+                               );
+               if ($terminate_control) {
+                   last;
+               }
+           }
+           else {
+               $errors++;
+               if (++$unknowns >= 5) {
+                   print {$transcript} "Too many unknown commands, stopping here.\n\n";
+                   last;
+               }
+           }
+       }
+    }
+    my $temp_transcript = $transcript_scalar;
+    eval{
+       $temp_transcript = decode("utf8",$temp_transcript,Encode::FB_CROAK);
+    };
+    my @maintccs = determine_recipients(recipients => \%recipients,
+                                       address_only => 1,
+                                       cc => 1,
+                                      );
+    my $error_text = $errors > 0 ? " (with $errors errors)":'';
+    my $reply =
+       create_mime_message(['X-Loop'      => $gMaintainerEmail,
+                            From          => "$gMaintainerEmail ($gProject $gBug Tracking System)",
+                            To            => $replyto,
+                            @maintccs ? (Cc => join(', ',@maintccs)):(),
+                            Subject       => "Processed${error_text}: $header{subject}",
+                            'Message-ID'  => "<handler.s.$nn.transcript\@$gEmailDomain>",
+                            'In-Reply-To' => $header{'message-id'},
+                            References    => join(' ',grep {defined $_} $header{'message-id'},$data->{msgid}),
+                            Precedence    => 'bulk',
+                            keys %affected_packages ?("X-${gProject}-PR-Package" => join(' ',keys %affected_packages)):(),
+                            keys %affected_packages ?("X-${gProject}-PR-Source" =>
+                                                      join(' ',
+                                                           map {defined $_ ?(ref($_)?@{$_}:$_):()}
+                                                           binary_to_source(binary => [keys %affected_packages],
+                                                                            source_only => 1))):(),
+                            "X-$gProject-PR-Message" => 'transcript',
+                            @common_headers,
+                           ],
+                           fill_template('mail/message_body',
+                                         {body => $temp_transcript},
+                                        ));
+
+    utime(time,time,"db-h");
+
+    send_mail_message(message => $reply,
+                     recipients => [exists $header{'x-debbugs-no-ack'}?():$replyto,
+                                    make_list(values %{{determine_recipients(recipients => \%recipients,
+                                                                             address_only => 1,
+                                                                            )}}
+                                             ),
+                                   ]
+                    );
+
+}
+
+
+finish();
 
 sub appendlog {
-    my $hash = get_hashname($ref);
-    if (!open(AP,">>db-h/$hash/$ref.log")) {
-        print DEBUG "failed open log<\n";
-        print DEBUG "failed open log err $!<\n";
-        die "opening db-h/$hash/$ref.log (li): $!";
-    }
-    print(AP "\7\n",escape_log(@log),"\n\3\n") || die "writing db-h/$hash/$ref.log (li): $!";
-    close(AP) || die "closing db-h/$hash/$ref.log (li): $!";
+    my ($ref,$msg) = @_;
+    my $log_location = buglog($ref);
+    die "Unable to find .log for $ref"
+       if not defined $log_location;
+    my $logfh = IO::File->new(">>$log_location") or
+       die "Unable to open $log_location for appending: $!";
+    write_log_records(logfh => $logfh,
+                     records => [{type => 'incoming-recv',
+                                  text => $msg,
+                                 }]);
+    close ($logfh) or die "Unable to close $log_location: $!";
 }
 
 sub finish {
@@ -1049,6 +1220,7 @@ sub fill_template{
      my $variables = {config => \%config,
                      defined($ref)?(ref    => $ref):(),
                      defined($data)?(data  => $data):(),
+                     refs => [map {exists $clonebugs{$_}?$clonebugs{$_}:$_} keys %bug_affected],
                      %{$extra_var},
                     };
      my $hole_var = {'&bugurl' =>
@@ -1092,7 +1264,7 @@ sub checkmaintainers {
        # this is utter hackery until we switch to Debbugs::Recipients
        my @maints = package_maintainer(binary => $p);
         if (@maints) {
-           print DEBUG "maintainer add >$p|".join(',',@maints)."<\n";
+           print {$debugfh} "maintainer add >$p|".join(',',@maints)."<\n";
            my %temp;
            @temp{@maintaddrs} = @maintaddrs;
             push(@maintaddrs,
@@ -1100,7 +1272,7 @@ sub checkmaintainers {
                           not exists $temp{$_}} @maints);
             $anymaintfound++;
         } else {
-           print DEBUG "maintainer none >$p<\n";
+           print {$debugfh} "maintainer none >$p<\n";
            push(@maintaddrs,$gUnknownMaintainerEmail) unless $anymaintnotfound;
             $anymaintnotfound++;
             last;
@@ -1108,7 +1280,7 @@ sub checkmaintainers {
     }
 
     if (defined $data->{owner} and length $data->{owner}) {
-        print DEBUG "owner add >$data->{package}|$data->{owner}<\n";
+        print {$debugfh} "owner add >$data->{package}|$data->{owner}<\n";
         my $addmaint = $data->{owner};
         push(@maintaddrs, $addmaint) unless
             $addmaint eq $replyto or grep($_ eq $addmaint, @maintaddrs);
@@ -1161,12 +1333,12 @@ sub bug_list_forward{
               if defined $data;
      print STDERR "Tried to loop me with $envelope_from\n"
          and exit 1 if $envelope_from =~ /\Q$gListDomain\E|\Q$gEmailDomain\E/;
-     print DEBUG $envelope_from,qq(\n);
+     print {$debugfh} $envelope_from,qq(\n);
      # If we don't have a bug address, something has gone horribly wrong.
      print STDERR "Doesn't match: $bug_address\n" and exit 1 unless defined $bug_address;
      $bug_address =~ s/\@.+//;
-     print DEBUG "Sending message to bugs=$bug_address\@$config{bug_subscription_domain}\n";
-     print DEBUG $header.qq(\n\n).$body;
+     print {$debugfh} "Sending message to bugs=$bug_address\@$config{bug_subscription_domain}\n";
+     print {$debugfh} $header.qq(\n\n).$body;
      send_mail_message(message        => $header.qq(\n\n).$body,
                       recipients     => ["bugs=$bug_address\@$config{bug_subscription_domain}"],
                       envelope_from  => $envelope_from,
index 29d14af789619d8a4dace8cdfad511bb8a2af909..4d20051f332b55437eb6060b7f71b8dcf3166751 100755 (executable)
@@ -31,6 +31,7 @@ use Debbugs::Status qw(splitpackages);
 
 use Debbugs::CGI qw(html_escape);
 use Debbugs::Control qw(:all);
+use Debbugs::Control::Service qw(:all);
 use Debbugs::Log qw(:misc);
 use Debbugs::Text qw(:templates);
 
@@ -169,11 +170,6 @@ my %clonebugs = ();
 my %bcc = ();
 
 
-my @bcc;
-sub addbcc {
-    push @bcc, $_[0] unless grep { $_ eq $_[0] } @bcc;
-}
-
 our $data;
 our $message;
 our $extramessage;
@@ -414,12 +410,13 @@ END
            print {$transcript} "No valid user selected\n";
            $errors++;
            $indicated_user = 1;
-        } elsif (&setbug) {
+        } elsif (check_limit(data => read_bug(bug => $ref),
+                            limit => \%limit,
+                            transcript => $transcript)) {
            if (not $indicated_user and defined $user) {
                 print {$transcript} "User is $user\n";
                 $indicated_user = 1;
            }
-           &nochangebug;
            my %ut;
            Debbugs::User::read_usertags(\%ut, $user);
             my @oldtags = (); my @newtags = (); my @badtags = ();
@@ -465,607 +462,22 @@ Unknown command or malformed arguments to command.
 (Use control\@$gEmailDomain to manipulate reports.)
 
 END
-       $errors++;
-        if (++$unknowns >= 3) {
-            print {$transcript} "Too many unknown commands, stopping here.\n\n";
-            last;
-        }
-#### "developer only" ones start here
-    } elsif (m/^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/i) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       if (defined $2) {
-           eval {
-               set_fixed(@common_control_options,
-                         bug   => $ref,
-                         fixed => $2,
-                         add   => 1,
-                        );
-           };
-           if ($@) {
-               $errors++;
-               print {$transcript} "Failed to add fixed version '$2' to $ref: ".cleanup_eval_fail($@,$debug)."\n";
-           }
-       }
-       eval {
-           set_done(@common_control_options,
-                    done      => 1,
-                    bug       => $ref,
-                    reopen    => 0,
-                    notify_submitter => 1,
-                    clear_fixed => 0,
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref as done: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^reassign\s+\#?(-?\d+)\s+ # bug and command
-              (?:(?:((?:src:|source:)?$config{package_name_re}) # new package
-              (?:\s+((?:$config{package_name_re}\/)?
-                      $config{package_version_re}))?)| # optional version
-              ((?:src:|source:)?$config{package_name_re} # multiple package form
-              (?:\s*\,\s*(?:src:|source:)?$config{package_name_re})+))
-              \s*$/xi) {
-        $ok++;
-        $ref= $1;
-       my @new_packages;
-       if (not defined $2) {
-           push @new_packages, split /\s*\,\s*/,$4;
-       }
-       else {
-           push @new_packages, $2;
-       }
-       @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-        my $version= $3;
-       eval {
-           set_package(@common_control_options,
-                       bug          => $ref,
-                       package      => \@new_packages,
-                      );
-           # if there is a version passed, we make an internal call
-           # to set_found
-           if (defined($version) && length $version) {
-               set_found(@common_control_options,
-                         bug   => $ref,
-                         found => $version,
-                        );
-           }
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^reopen\s+\#?(-?\d+)(?:\s+([\=\!]|(?:\S.*\S)))?$/i) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my $new_submitter = $2;
-       if (defined $new_submitter) {
-           if ($new_submitter eq '=') {
-               undef $new_submitter;
-           }
-           elsif ($new_submitter eq '!') {
-               $new_submitter = $replyto;
-           }
-       }
-       eval {
-           set_done(@common_control_options,
-                    bug          => $ref,
-                    reopen       => 1,
-                    defined $new_submitter? (submitter    => $new_submitter):(),
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to reopen $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m{^(?:(?i)found)\s+\#?(-?\d+)
-              (?:\s+((?:$config{package_name_re}\/)?
-                   $config{package_version_re}
-               # allow for multiple packages
-               (?:\s*,\s*(?:$config{package_name_re}\/)?
-                   $config{package_version_re})*)
-           )?$}x) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my @versions;
-        if (defined $2) {
-           @versions = split /\s*,\s*/,$2;
-           eval {
-               set_found(@common_control_options,
-                         bug          => $ref,
-                         found        => \@versions,
-                         add          => 1,
-                        );
-           };
-           if ($@) {
-               $errors++;
-               print {$transcript} "Failed to add found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-           }
-       }
-       else {
-           eval {
-               set_fixed(@common_control_options,
-                         bug          => $ref,
-                         fixed        => [],
-                         reopen       => 1,
-                        );
-           };
-           if ($@) {
-               $errors++;
-               print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-           }
-       }
-    }
-    elsif (m{^(?:(?i)notfound)\s+\#?(-?\d+)
-            \s+((?:$config{package_name_re}\/)?
-                $config{package_version_re}
-               # allow for multiple packages
-               (?:\s*,\s*(?:$config{package_name_re}\/)?
-                   $config{package_version_re})*
-           )$}x) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my @versions;
-        @versions = split /\s*,\s*/,$2;
-       eval {
-           set_found(@common_control_options,
-                     bug          => $ref,
-                     found        => \@versions,
-                     remove       => 1,
-                    );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to remove found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    }
-    elsif (m{^(?:(?i)fixed)\s+\#?(-?\d+)
-            \s+((?:$config{package_name_re}\/)?
-                   $config{package_version_re}
-               # allow for multiple packages
-               (?:\s*,\s*(?:$config{package_name_re}\/)?
-                   $config{package_version_re})*)
-           \s*$}x) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my @versions;
-        @versions = split /\s*,\s*/,$2;
-       eval {
-           set_fixed(@common_control_options,
-                     bug          => $ref,
-                     fixed        => \@versions,
-                     add          => 1,
-                    );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to add fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    }
-    elsif (m{^(?:(?i)notfixed)\s+\#?(-?\d+)
-            \s+((?:$config{package_name_re}\/)?
-                   $config{package_version_re}
-               # allow for multiple packages
-               (?:\s*,\s*(?:$config{package_name_re}\/)?
-                   $config{package_version_re})*)
-           \s*$}x) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my @versions;
-        @versions = split /\s*,\s*/,$2;
-       eval {
-           set_fixed(@common_control_options,
-                     bug          => $ref,
-                     fixed        => \@versions,
-                     remove       => 1,
-                    );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to remove fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    }
-    elsif (m/^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/i) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my $newsubmitter = $2 eq '!' ? $replyto : $2;
-        if (not Mail::RFC822::Address::valid($newsubmitter)) {
-            print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n";
-            $errors++;
-       }
-       else {
-           eval {
-               set_submitter(@common_control_options,
-                             bug       => $ref,
-                             submitter => $newsubmitter,
-                            );
-           };
-           if ($@) {
-               $errors++;
-               print {$transcript} "Failed to set submitter on $ref: ".cleanup_eval_fail($@,$debug)."\n";
-           }
-        }
-    } elsif (m/^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/i) {
-        $ok++;
-        $ref= $1;
-       my $forward_to= $2;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-           set_forwarded(@common_control_options,
-                         bug          => $ref,
-                         forwarded    => $forward_to,
-                          );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^notforwarded\s+\#?(-?\d+)$/i) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-           set_forwarded(@common_control_options,
-                         bug          => $ref,
-                         forwarded    => undef,
-                          );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to clear the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) {
-        $ok++;
-        $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-        my $newseverity= $2;
-        if (exists $gObsoleteSeverities{$newseverity}) {
-            print {$transcript} "Severity level \`$newseverity' is obsolete. " .
-                "Use $gObsoleteSeverities{$newseverity} instead.\n\n";
-               $errors++;
-        } elsif (not defined first {$_ eq $newseverity}
-           (@gSeverityList, "$gDefaultSeverity")) {
-            print {$transcript} "Severity level \`$newseverity' is not known.\n".
-                 "Recognized are: $gShowSeverities.\n\n";
-           $errors++;
-        } else {
-           eval {
-               set_severity(@common_control_options,
-                            bug => $ref,
-                            severity => $newseverity,
-                           );
-           };
-           if ($@) {
-               $errors++;
-               print {$transcript} "Failed to set severity of $config{bug} $ref to $newseverity: ".cleanup_eval_fail($@,$debug)."\n";
-           }
-       }
-    } elsif (m/^tags?\s+\#?(-?\d+)\s+(\S.*)$/i) {
-       $ok++;
-       $ref = $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my $tags = $2;
-       my @tags = map {m/^([+=-])(.+)/ ? ($1,$2):($_)} split /[\s,]+/, $tags;
-       # this is an array of hashrefs which contain two elements, the
-       # first of which is the array of tags, the second is the
-       # option to pass to set_tag (we use a hashref here to make it
-       # more obvious what is happening)
-       my @tag_operations;
-       my @badtags;
-       for my $tag (@tags) {
-           if ($tag =~ /^[=+-]$/) {
-               if ($tag eq '=') {
-                   @tag_operations = {tags => [],
-                                      option => [],
-                                     };
-               }
-               elsif ($tag eq '-') {
-                   push @tag_operations,
-                       {tags => [],
-                        option => [remove => 1],
-                       };
-               }
-               elsif ($tag eq '+') {
-                   push @tag_operations,
-                       {tags => [],
-                        option => [add => 1],
-                       };
-               }
-               next;
-           }
-           if (not defined first {$_ eq $tag} @{$config{tags}}) {
-               push @badtags, $tag;
-               next;
-           }
-           if (not @tag_operations) {
-               @tag_operations = {tags => [],
-                                  option => [add => 1],
-                                 };
-           }
-           push @{$tag_operations[-1]{tags}},$tag;
-       }
-       if (@badtags) {
-            print {$transcript} "Unknown tag/s: ".join(', ', @badtags).".\n".
-                "Recognized are: ".join(' ', @gTags).".\n\n";
-           $errors++;
-       }
-       eval {
-           for my $operation (@tag_operations) {
-               set_tag(@common_control_options,
-                       bug => $ref,
-                       tag => [@{$operation->{tags}}],
-                       warn_on_bad_tags => 0, # don't warn on bad tags,
-                       # 'cause we do that above
-                       @{$operation->{option}},
-                      );
-           }
-       };
-       if ($@) {
-           # we intentionally have two errors here if there is a bad
-           # tag and the above fails for some reason
-           $errors++;
-           print {$transcript} "Failed to alter tags of $config{bug} $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/i) {
-       $ok++;
-        $ref= $2;
-       my $add_remove = defined $1 && $1 eq 'un';
-       my @blockers = map {exists $clonebugs{$_}?$clonebugs{$_}:$_} split /[\s,]+/, $3;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-            set_blocks(@common_control_options,
-                       bug          => $ref,
-                       block        => \@blockers,
-                       $add_remove ? (remove => 1):(add => 1),
-                      );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set blocking bugs of $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/i) {
-        $ok++;
-        $ref= $1; my $newtitle= $2;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-            set_title(@common_control_options,
-                      bug          => $ref,
-                      title        => $newtitle,
-                     );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set the title of $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^unmerge\s+\#?(-?\d+)$/i) {
-       $ok++;
-       $ref= $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-            set_merged(@common_control_options,
-                       bug          => $ref,
-                      );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to unmerge $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^merge\s+#?(-?\d+(\s+#?-?\d+)+)\s*$/i) {
-       $ok++;
-       my @tomerge;
-        ($ref,@tomerge) = map {exists $clonebugs{$_}?$clonebugs{$_}:$_}
-           split(/\s+#?/,$1);
-       eval {
-            set_merged(@common_control_options,
-                       bug          => $ref,
-                       merge_with   => \@tomerge,
-                      );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to merge $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^forcemerge\s+\#?(-?\d+(?:\s+\#?-?\d+)+)\s*$/i) {
-       $ok++;
-       my @tomerge;
-        ($ref,@tomerge) = map {exists $clonebugs{$_}?$clonebugs{$_}:$_}
-           split(/\s+#?/,$1);
-       eval {
-            set_merged(@common_control_options,
-                       bug          => $ref,
-                       merge_with   => \@tomerge,
-                       force        => 1,
-                       masterbug    => 1,
-                      );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to forcibly merge $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^clone\s+#?(\d+)\s+((-\d+\s+)*-\d+)\s*$/i) {
-       $ok++;
-
-       my $origref = $1;
-       my @newclonedids = split /\s+/, $2;
-       my $newbugsneeded = scalar(@newclonedids);
-
-       $ref = $origref;
-       if (exists $clonebugs{$ref}) {
-           $ref = $clonebugs{$ref};
-       }
-       $bug_affected{$ref} = 1;
-       eval {
-           my %new_clones;
-           clone_bug(@common_control_options,
-                     bug => $ref,
-                     new_bugs => \@newclonedids,
-                     new_clones => \%new_clones,
-                    );
-           %clonebugs = (%clonebugs,
-                         %new_clones);
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to clone $ref: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^package\:?\s+(\S.*\S)?\s*$/i) {
-        $ok++;
-       my @pkgs = split /\s+/, $1;
-       if (scalar(@pkgs) > 0) {
-               %limit_pkgs = map { ($_, 1) } @pkgs;
-               $limit{package} = [@pkgs];
-               print {$transcript} "Limiting to bugs with field 'package' containing at least one of ".join(', ',map {qq('$_')} @pkgs)."\n";
-               print {$transcript} "Limit currently set to";
-               for my $limit_field (keys %limit) {
-                   print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n";
-               }
-               print {$transcript} "\n";
-       } else {
-           %limit_pkgs = ();
-           $limit{package} = [];
-           print {$transcript} "Limit cleared.\n\n";
-       }
-    } elsif (m/^limit\:?\s+(\S.*\S)\s*$/) {
-       $ok++;
-       my ($field,@options) = split /\s+/, $1;
-       $field = lc($field);
-       if ($field =~ /^(?:clear|unset|blank)$/) {
-           %limit = ();
-           print {$transcript} "Limit cleared.\n\n";
-       }
-       elsif (exists $Debbugs::Status::fields{$field} or $field eq 'source') {
-           # %limit can actually contain regexes, but because they're
-           # not evaluated in Safe, DO NOT allow them through without
-           # fixing this.
-           $limit{$field} = [@options];
-           print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n";
-           print {$transcript} "Limit currently set to";
-           for my $limit_field (keys %limit) {
-               print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n";
-           }
-           print {$transcript} "\n";
-       }
-       else {
-           print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n";
-           $errors++;
+       #### "developer only" ones start here
+    } elsif (defined valid_control($_)) {
+       my ($new_errors,$terminate_control) =
+           control_line(line => $_,
+                        clonebugs => \%clonebugs,
+                        limit => \%limit,
+                        common_control_options => \@common_control_options,
+                        errors => \$errors,
+                        transcript => $transcript,
+                        debug => $debug,
+                        ok => \$ok,
+                       );
+       if ($terminate_control) {
            last;
        }
-    } elsif (m/^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/i) {
-       $ok++;
-        $ref = $1;
-       my $add_remove = $2;
-       my $packages = $3;
-       # if there isn't a package given, assume that we should unset
-       # affects; otherwise default to adding
-       if (not defined $packages or
-           not length $packages) {
-           $packages = '';
-           $add_remove ||= '=';
-       }
-       elsif (not defined $add_remove or
-              not length $add_remove) {
-           $add_remove = '+';
-       }
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-            affects(@common_control_options,
-                    bug => $ref,
-                    package     => [splitpackages($3)],
-                    ($add_remove eq '+'?(add => 1):()),
-                    ($add_remove eq '-'?(remove => 1):()),
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n";
-       }
-
-    } elsif (m/^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/i) {
-       $ok++;
-        $ref = $1;
-       my $summary_msg = length($2)?$2:undef;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-           summary(@common_control_options,
-                   bug          => $ref,
-                   summary      => $summary_msg,
-                  );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-
-    } elsif (m/^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/i) {
-       $ok++;
-        $ref = $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       my $newowner = $2;
-       if ($newowner eq '!') {
-           $newowner = $replyto;
-       }
-       eval {
-           owner(@common_control_options,
-                 bug          => $ref,
-                 owner        => $newowner,
-                );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^noowner\s+\#?(-?\d+)\s*$/i) {
-        $ok++;
-        $ref = $1;
-       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-       eval {
-           owner(@common_control_options,
-                 bug          => $ref,
-                 owner        => undef,
-                );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n";
-       }
-    } elsif (m/^unarchive\s+#?(\d+)$/i) {
-        $ok++;
-        $ref = $1;
-        $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-        eval {
-             bug_unarchive(@common_control_options,
-                           bug        => $ref,
-                           recipients => \%recipients,
-                          );
-        };
-        if ($@) {
-             $errors++;
-        }
-    } elsif (m/^archive\s+#?(\d+)$/i) {
-        $ok++;
-        $ref = $1;
-        $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
-        eval {
-             bug_archive(@common_control_options,
-                         bug => $ref,
-                         ignore_time => 1,
-                         archive_unarchived => 0,
-                        );
-        };
-        if ($@) {
-             $errors++;
-        }
     } else {
-        print {$transcript} "Unknown command or malformed arguments to command.\n\n";
        $errors++;
         if (++$unknowns >= 5) {
             print {$transcript} "Too many unknown commands, stopping here.\n\n";
@@ -1087,19 +499,6 @@ my @maintccs = determine_recipients(recipients => \%recipients,
                                    address_only => 1,
                                    cc => 1,
                                   );
-my $maintccs = 'Cc: '.join(",\n    ",
-                   determine_recipients(recipients => \%recipients,
-                                        cc => 1,
-                                       )
-                  )."\n";
-
-my $packagepr = '';
-$packagepr = "X-${gProject}-PR-Package: " . join(keys %affected_packages) . "\n" if keys %affected_packages;
-
-# Add Bcc's to subscribed bugs
-# now handled by Debbugs::Recipients
-#push @bcc, map {"bugs=$_\@$gListDomain"} keys %bug_affected;
-
 if (!defined $header{'subject'} || $header{'subject'} eq "") {
   $header{'subject'} = "your mail";
 }
@@ -1110,7 +509,7 @@ my $error_text = $errors > 0 ? " (with $errors errors)":'';
 my @common_headers;
 push @common_headers, 'X-Loop',$gMaintainerEmail;
 
-my $temp_transcript = ${transcript_scalar};
+my $temp_transcript = $transcript_scalar;
 eval{
     $temp_transcript = decode("utf8",$temp_transcript,Encode::FB_CROAK);
 };
@@ -1143,19 +542,6 @@ my $repliedshow= join(', ',$replyto,
                                          )
                     );
 
-# -1 is the service.in log
-&filelock("lock/-1");
-open(AP,">>db-h/-1.log") || die "open db-h/-1.log: $!";
-print(AP
-      "\2\n$repliedshow\n\5\n$reply\n\3\n".
-      "\6\n".
-      "<strong>Request received</strong> from <code>".
-      html_escape($header{'from'})."</code>\n".
-      "to <code>".html_escape($controlrequestaddr)."</code>\n".
-      "\3\n".
-      "\7\n",escape_log(@log),"\n\3\n") || die "writing db-h/-1.log: $!";
-close(AP) || die "open db-h/-1.log: $!";
-&unfilelock;
 utime(time,time,"db-h");
 
 &sendmailmessage($reply,
diff --git a/t/14_control_at_submit.t b/t/14_control_at_submit.t
new file mode 100644 (file)
index 0000000..7f0659f
--- /dev/null
@@ -0,0 +1,353 @@
+# -*- mode: cperl;-*-
+# $Id: 05_mail.t,v 1.1 2005/08/17 21:46:17 don Exp $
+
+use Test::More tests => 118;
+
+use warnings;
+use strict;
+
+# Here, we're going to shoot messages through a set of things that can
+# happen.
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+# The test functions are placed here to make things easier
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+use Data::Dumper;
+use Encode qw(decode encode);
+
+# HTTP::Server:::Simple defines a SIG{CHLD} handler that breaks system; undef it here.
+$SIG{CHLD} = sub {};
+my %config;
+eval {
+     %config = create_debbugs_configuration(debug => exists $ENV{DEBUG}?$ENV{DEBUG}:0);
+};
+if ($@) {
+     BAIL_OUT($@);
+}
+
+my $sendmail_dir = $config{sendmail_dir};
+my $spool_dir = $config{spool_dir};
+my $config_dir = $config{config_dir};
+
+END{
+     if ($ENV{DEBUG}) {
+         diag("spool_dir:   $spool_dir\n");
+         diag("config_dir:   $config_dir\n");
+         diag("sendmail_dir: $sendmail_dir\n");
+     }
+}
+
+# We're going to use create mime message to create these messages, and
+# then just send them to receive.
+
+send_message(to=>'submit@bugs.something',
+            headers => [To   => 'submit@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Submiting a bug',
+                       ],
+            body => <<EOF) or fail('Unable to send message');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+# now we check to see that we have a bug, and nextnumber has been incremented
+ok(-e "$spool_dir/db-h/01/1.log",'log file created');
+ok(-e "$spool_dir/db-h/01/1.summary",'sumary file created');
+ok(-e "$spool_dir/db-h/01/1.status",'status file created');
+ok(-e "$spool_dir/db-h/01/1.report",'report file created');
+
+# next, we check to see that (at least) the proper messages have been
+# sent out. 1) ack to submitter 2) mail to maintainer
+
+# This keeps track of the previous size of the sendmail directory
+my $SD_SIZE = 0;
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,2,
+                     $sendmail_dir,
+                     'submit messages appear to have been sent out properly',
+                    );
+
+
+# now send a message to the bug
+
+send_message(to => '1@bugs.something',
+            headers => [To   => '1@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Sending a message to a bug',
+                       ],
+            body => <<EOF) or fail('sending message to 1@bugs.someting failed');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,2,
+                     $sendmail_dir,
+                     '1@bugs.something messages appear to have been sent out properly');
+
+# just check to see that control doesn't explode
+send_message(to => 'control@bugs.something',
+            headers => [To   => 'control@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Munging a bug',
+                       ],
+            body => <<EOF) or fail 'message to control@bugs.something failed';
+severity 1 wishlist
+retitle 1 new title
+thanks
+EOF
+
+$SD_SIZE =
+   num_messages_sent($SD_SIZE,1,
+                    $sendmail_dir,
+                    'control@bugs.something messages appear to have been sent out properly');
+# now we need to check to make sure the control message was processed without errors
+ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug")) == 0,
+   'control@bugs.something message was parsed without errors');
+# now we need to check to make sure that the control message actually did anything
+# This is an eval because $ENV{DEBBUGS_CONFIG_FILE} isn't set at BEGIN{} time
+eval "use Debbugs::Status qw(read_bug writebug);";
+my $status = read_bug(bug=>1);
+ok($status->{subject} eq 'new title','bug 1 retitled');
+ok($status->{severity} eq 'wishlist','bug 1 wishlisted');
+
+# now we're going to go through and methododically test all of the control commands.
+my @control_commands =
+     (
+       clone        => {command => 'clone',
+                      value   => '-2',
+                      status_key => 'package',
+                      status_value => 'foo',
+                      bug          => '3',
+                     },
+       severity_wishlist => {command => 'severity',
+                           value   => 'wishlist',
+                           status_key => 'severity',
+                           status_value => 'wishlist',
+                          },
+       reassign_bar_baz => {command => 'reassign',
+                          value   => 'bar,baz',
+                          status_key => 'package',
+                          status_value => 'bar,baz',
+                         },
+       reassign_foo => {command => 'reassign',
+                      value   => 'foo',
+                      status_key => 'package',
+                      status_value => 'foo',
+                     },
+       'found_1.0'        => {command => 'found',
+                            value   => '1.0',
+                            status_key => 'found_versions',
+                            status_value => ['1.0'],
+                           },
+       'notfound_1.0'     => {command => 'notfound',
+                            value   => '1.0',
+                            status_key => 'found_versions',
+                            status_value => [],
+                           },
+       'found_1.0~5+1b2'  => {command => 'found',
+                            value   => '1.0~5+1b2',
+                            status_key => 'found_versions',
+                            status_value => ['1.0~5+1b2'],
+                           },
+       'notfound_1.0~5+1b2' => {command => 'notfound',
+                              value   => '1.0~5+1b2',
+                              status_key => 'found_versions',
+                              status_value => [],
+                             },
+       'fixed_1.1'        => {command => 'fixed',
+                            value   => '1.1',
+                            status_key => 'fixed_versions',
+                            status_value => ['1.1'],
+                           },
+       'notfixed_1.1'     => {command => 'notfixed',
+                            value   => '1.1',
+                            status_key => 'fixed_versions',
+                            status_value => [],
+                           },
+       'found_1.0~5+1b2'  => {command => 'found',
+                            value   => '1.0~5+1b2',
+                            status_key => 'found_versions',
+                            status_value => ['1.0~5+1b2'],
+                           },
+       'fixed_1.2'        => {command => 'fixed',
+                            value   => '1.2',
+                            status_key => 'fixed_versions',
+                            status_value => ['1.2'],
+                           },
+       close              => {command => 'close',
+                            value   => '',
+                            status_key => 'done',
+                            status_value => 'foo@bugs.something',
+                           },
+       'found_1.3'        => {command => 'found',
+                            value   => '1.3',
+                            status_key => 'done',
+                            status_value => '',
+                           },
+       submitter_foo      => {command => 'submitter',
+                            value   => 'foo@bar.com',
+                            status_key => 'originator',
+                            status_value => 'foo@bar.com',
+                           },
+       forwarded_foo      => {command => 'forwarded',
+                            value   => 'foo@bar.com',
+                            status_key => 'forwarded',
+                            status_value => 'foo@bar.com',
+                           },
+       notforwarded       => {command => 'notforwarded',
+                            value   => '',
+                            status_key => 'forwarded',
+                            status_value => '',
+                           },
+       owner_foo          => {command => 'owner',
+                            value   => 'foo@bar.com',
+                            status_key => 'owner',
+                            status_value => 'foo@bar.com',
+                           },
+       noowner      => {command => 'noowner',
+                      value   => '',
+                      status_key => 'owner',
+                      status_value => '',
+                     },
+       merge        => {command => 'merge',
+                      value   => '1 3',
+                      status_key => 'mergedwith',
+                      status_value => '3',
+                     },
+       unmerge      => {command => 'unmerge',
+                      value   => '',
+                      status_key => 'mergedwith',
+                      status_value => '',
+                     },
+       forcemerge   => {command => 'forcemerge',
+                      value   => '3',
+                      status_key => 'mergedwith',
+                      status_value => '3',
+                     },
+       unmerge      => {command => 'unmerge',
+                      value   => '',
+                      status_key => 'mergedwith',
+                      status_value => '',
+                     },
+       block        => {command => 'block',
+                      value   => ' with 2',
+                      status_key => 'blockedby',
+                      status_value => '2',
+                     },
+       unblock      => {command => 'unblock',
+                      value   => ' with 2',
+                      status_key => 'blockedby',
+                      status_value => '',
+                     },
+       summary      => {command => 'summary',
+                      value   => '5',
+                      status_key => 'summary',
+                      status_value => 'This is a silly bug',
+                     },
+       nosummary    => {command => 'summary',
+                      value   => '',
+                      status_key => 'summary',
+                      status_value => '',
+                     },
+       affects      => {command => 'affects',
+                      value   => 'foo',
+                      status_key => 'affects',
+                      status_value => 'foo',
+                     },
+       noaffects    => {command => 'affects',
+                      value   => '',
+                      status_key => 'affects',
+                      status_value => '',
+                     },
+       close        => {command => 'close',
+                      value   => '',
+                      status_key => 'done',
+                      status_value => 'foo@bugs.something',
+                     },
+       archive      => {command => 'archive',
+                      value   => '',
+                      status_key => 'owner',
+                      status_value => '',
+                      location => 'archive',
+                     },
+       unarchive    => {command => 'unarchive',
+                      value   => '',
+                      status_key => 'owner',
+                      status_value => '',
+                     },
+       tag          => {command => 'tag',
+                      value   => ' = patch',
+                      status_key => 'keywords',
+                      status_value => 'patch',
+                     },
+       untag        => {command => 'tag',
+                      value   => ' - patch',
+                      status_key => 'keywords',
+                      status_value => '',
+                     },
+       plustag      => {command => 'tag',
+                      value   => ' + patch',
+                      status_key => 'keywords',
+                      status_value => 'patch',
+                     },
+       utf8_retitle => {command => 'retitle',
+                      value   => 'Thïs is a ütff8 title [♥♡☙☎]',
+                      status_key => 'subject',
+                      status_value => decode("utf8",'Thïs is a ütff8 title [♥♡☙☎]'),
+                     },
+     );
+
+# In order for the archive/unarchive to work, we have to munge the summary file slightly
+$status = read_bug(bug => 1);
+$status->{unarchived} = time;
+writebug(1,$status);
+while (my ($command,$control_command) = splice(@control_commands,0,2)) {
+     # just check to see that control doesn't explode
+     $control_command->{value} = " $control_command->{value}" if length $control_command->{value}
+         and $control_command->{value} !~ /^\s/;
+     send_message(to => 'submit@bugs.something',
+                 headers => [To   => 'submit@bugs.something',
+                             From => 'foo@bugs.something',
+                             Subject => "Munging a bug with $command",
+                            ],
+                 body => <<EOF) or fail 'message to control@bugs.something failed';
+Package: foo
+Control: $control_command->{command} 1$control_command->{value}
+EOF
+                                 ;
+     $SD_SIZE =
+        num_messages_sent($SD_SIZE,1,
+                          $sendmail_dir,
+                          'control@bugs.something messages appear to have been sent out properly');
+     # now we need to check to make sure the control message was processed without errors
+     ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug with $command")) == 0,
+       'control@bugs.something'. "$command message was parsed without errors");
+     # now we need to check to make sure that the control message actually did anything
+     my $status;
+     $status = read_bug(exists $control_command->{bug}?(bug => $control_command->{bug}):(bug=>1),
+                       exists $control_command->{location}?(location => $control_command->{location}):(),
+                      );
+     is_deeply($status->{$control_command->{status_key}},
+              $control_command->{status_value},
+              "bug " .
+              (exists $control_command->{bug}?$control_command->{bug}:1).
+              " $command"
+             )
+         or fail(Dumper($status));
+}
+