1#!/usr/bin/perl
2
3# mass-bug: mass-file a bug report against a list of packages
4# For options, see the usage message below.
5#
6# Copyright 2006 by Joey Hess <joeyh@debian.org>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program. If not, see <https://www.gnu.org/licenses/>.
20
21=head1 NAME
22
23mass-bug - mass-file a bug report against a list of packages
24
25=head1 SYNOPSIS
26
27B<mass-bug> [I<options>] B<--subject=">I<bug subject>B<"> I<template package-list>
28
29=head1 DESCRIPTION
30
31mass-bug assists in filing a mass bug report in the Debian BTS on a set of
32packages. For each package in the package-list file (which should list one
33package per line together with an optional version number separated
34from the package name by an underscore), it fills out the template, adds
35BTS pseudo-headers, and either displays or sends the bug report.
36
37Warning: Some care has been taken to avoid unpleasant and common mistakes,
38but this is still a power tool that can generate massive amounts of bug
39report mails. Use it with care, and read the documentation in the
40Developer's Reference about mass filing of bug reports first.
41
42=head1 TEMPLATE
43
44The template file is the body of the message that will be sent for each bug
45report, excluding the BTS pseudo-headers. In the template, #PACKAGE# is
46replaced with the name of the package. If a version was specified for
47the package, #VERSION# will be replaced by that version.
48
49The components of the version number may be specified using #EPOCH#,
50#UPSTREAM_VERSION# and #REVISION#. #EPOCH# includes the trailing colon and
51#REVISION# the leading dash so that #EPOCH#UPSTREAM_VERSION##REVISION# is
52always the same as #VERSION#.
53
54Note that text in the template will be automatically word-wrapped to 70
55columns, up to the start of a signature (indicated by S<'-- '> at the
56start of a line on its own). This is another reason to avoid including
57BTS pseudo-headers in your template.
58
59=head1 OPTIONS
60
61B<mass-bug> examines the B<devscripts> configuration files as described
62below.  Command line options override the configuration file settings,
63though.
64
65=over 4
66
67=item B<--severity=>(B<wishlist>|B<minor>|B<normal>|B<important>|B<serious>|B<grave>|B<critical>)
68
69Specify the severity with which bugs should be filed. Default
70is B<normal>.
71
72=item B<--display>
73
74Fill out the templates for each package and display them all for
75verification. This is the default behavior.
76
77=item B<--send>
78
79Actually send the bug reports.
80
81=item B<--subject=">I<bug subject>B<">
82
83Specify the subject of the bug report. The subject will be automatically
84prefixed with the name of the package that the bug is filed against.
85
86=item B<--tags>
87
88Set the BTS pseudo-header for tags.
89
90=item B<--user>
91
92Set the BTS pseudo-header for a usertags' user.
93
94=item B<--usertags>
95
96Set the BTS pseudo-header for usertags.
97
98=item B<--control=>I<COMMAND>
99
100Add a BTS control command. This option may be repeated to add multiple
101control commands. For example, if you are mass-bug-filing "please stop
102depending on this deprecated package", and bug 123456 represents removal
103of the deprecated package, you could use:
104
105    mass-bug --control='block 123456 by -1' ...
106
107=item B<--source>
108
109Specify that package names refer to source packages rather than binary
110packages.
111
112=item B<--sendmail=>I<SENDMAILCMD>
113
114Specify the B<sendmail> command.  The command will be split on white
115space and will not be passed to a shell.  Default is F</usr/sbin/sendmail>.
116
117=item B<--no-wrap>
118
119Do not wrap the template to lines of 70 characters.
120
121=item B<--no-conf>, B<--noconf>
122
123Do not read any configuration files.  This can only be used as the
124first option given on the command-line.
125
126=item B<--help>
127
128Provide a usage message.
129
130=item B<--version>
131
132Display version information.
133
134=back
135
136=head1 ENVIRONMENT
137
138B<DEBEMAIL> and B<EMAIL> can be set in the environment to control the email
139address that the bugs are sent from.
140
141=head1 CONFIGURATION VARIABLES
142
143The two configuration files F</etc/devscripts.conf> and
144F<~/.devscripts> are sourced by a shell in that order to set
145configuration variables.  Command line options can be used to override
146configuration file settings.  Environment variable settings are
147ignored for this purpose.  The currently recognised variables are:
148
149=over 4
150
151=item B<BTS_SENDMAIL_COMMAND>
152
153If this is set, specifies a B<sendmail> command to use instead of
154F</usr/sbin/sendmail>.  Same as the B<--sendmail> command line option.
155
156=back
157
158=cut
159
160use strict;
161use warnings;
162use Getopt::Long qw(:config bundling permute no_getopt_compat);
163use Text::Wrap;
164use File::Basename;
165use POSIX qw(locale_h strftime);
166
167setlocale(LC_TIME, "C");    # so that strftime is locale independent
168
169my $progname = basename($0);
170$Text::Wrap::columns = 70;
171my $submission_email = "maintonly\@bugs.debian.org";
172my $sendmailcmd      = '/usr/sbin/sendmail';
173my $modified_conf_msg;
174my %versions;
175
176sub usageerror {
177    die
178"Usage: $progname [options] --subject=\"bug subject\" <template> <package-list>\n";
179}
180
181sub usage {
182    print <<"EOT";
183Usage:
184  $progname [options] --subject="bug subject" <template> <package-list>
185
186Valid options are:
187   --display              Display the messages but don\'t send them
188   --send                 Actually send the mass bug reports to the BTS
189   --subject="bug subject"
190                          Text for email subject line (will be prefixed
191                          with "package: ")
192   --severity=(wishlist|minor|normal|important|serious|grave|critical)
193                          Specify the severity of the bugs to be filed
194                          (default "normal")
195
196   --tags=tags            Set the BTS pseudo-header for tags.
197   --user=user            Set the BTS pseudo-header for a usertags' user
198   --usertags=usertags    Set the BTS pseudo-header for usertags
199   --control="COMMAND"    Add an arbitrary BTS control command (repeatable)
200   --source               Specify that package names refer to source packages
201
202   --sendmail=cmd         Sendmail command to use (default /usr/sbin/sendmail)
203   --no-wrap              Don't wrap the template to 70 chars.
204   --no-conf, --noconf    Don\'t read devscripts config files;
205                          must be the first option given
206   --help                 Display this message
207   --version              Display version and copyright info
208
209   <template>             File containing email template; #PACKAGE# will
210                          be replaced by the package name and #VERSION#
211			  with the corresponding version (or a blank
212			  string if the version was not specified)
213   <package-list>         File containing list of packages, one per line
214			  in the format package(_version)
215
216  Ensure that you read the Developer\'s Reference on mass-filing bugs before
217  using this script!
218
219Default settings modified by devscripts configuration files:
220$modified_conf_msg
221EOT
222}
223
224sub version () {
225    print <<"EOF";
226This is $progname, from the Debian devscripts package, version ###VERSION###
227This code is copyright 2006 by Joey Hess, all rights reserved.
228This program comes with ABSOLUTELY NO WARRANTY.
229You are free to redistribute this code under the terms of the
230GNU General Public License, version 2 or later.
231EOF
232}
233
234# Next, read read configuration files and then command line
235# The next stuff is boilerplate
236
237if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
238    $modified_conf_msg = "  (no configuration files read)";
239    shift;
240} else {
241    my @config_files   = ('/etc/devscripts.conf', '~/.devscripts');
242    my %config_vars    = ('BTS_SENDMAIL_COMMAND' => '/usr/sbin/sendmail',);
243    my %config_default = %config_vars;
244
245    my $shell_cmd;
246    # Set defaults
247    foreach my $var (keys %config_vars) {
248        $shell_cmd .= qq[$var="$config_vars{$var}";\n];
249    }
250    $shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
251    $shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
252    # Read back values
253    foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
254    my $shell_out = `/bin/bash -c '$shell_cmd'`;
255    @config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
256
257    # Check validity
258    $config_vars{'BTS_SENDMAIL_COMMAND'} =~ /./
259      or $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail';
260
261    if ($config_vars{'BTS_SENDMAIL_COMMAND'} ne '/usr/sbin/sendmail') {
262        my $cmd = (split ' ', $config_vars{'BTS_SENDMAIL_COMMAND'})[0];
263        unless ($cmd =~ /^~?[A-Za-z0-9_\-\+\.\/]*$/) {
264            warn
265"BTS_SENDMAIL_COMMAND contained funny characters: $cmd\nReverting to default value /usr/sbin/sendmail\n";
266            $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail';
267        } elsif (system("command -v $cmd >/dev/null 2>&1") != 0) {
268            warn
269"BTS_SENDMAIL_COMMAND $cmd could not be executed.\nReverting to default value /usr/sbin/sendmail\n";
270            $config_vars{'BTS_SENDMAIL_COMMAND'} = '/usr/sbin/sendmail';
271        }
272    }
273
274    foreach my $var (sort keys %config_vars) {
275        if ($config_vars{$var} ne $config_default{$var}) {
276            $modified_conf_msg .= "  $var=$config_vars{$var}\n";
277        }
278    }
279    $modified_conf_msg ||= "  (none)\n";
280    chomp $modified_conf_msg;
281
282    $sendmailcmd = $config_vars{'BTS_SENDMAIL_COMMAND'};
283}
284
285sub gen_subject {
286    my $subject = shift;
287    my $package = shift;
288
289    return "$package\: $subject";
290}
291
292sub gen_bug {
293    my $template_text = shift;
294    my $package       = shift;
295    my $severity      = shift;
296    my $tags          = shift;
297    my $user          = shift;
298    my $usertags      = shift;
299    my $nowrap        = shift;
300    my $type          = shift;
301    my $control       = shift;
302    my $version       = "";
303    my $bugtext;
304
305    $version = $versions{$package} || "";
306
307    my ($epoch, $upstream, $revision)
308      = ($version =~ /^(\d+:)?(.+?)(-[^-]+)?$/);
309    $epoch    ||= "";
310    $revision ||= "";
311
312    $template_text =~ s/#PACKAGE#/$package/g;
313    $template_text =~ s/#VERSION#/$version/g;
314    $template_text =~ s/#EPOCH#/$epoch/g;
315    $template_text =~ s/#UPSTREAM_VERSION#/$upstream/g;
316    $template_text =~ s/#REVISION#/$revision/g;
317
318    $version = "Version: $version\n" if $version;
319
320    unless ($nowrap) {
321        if ($template_text =~ /\A(.*?)(^-- $)(.*)/ms)
322        {    # there's a sig involved
323            my ($presig, $sig) = ($1, $2 . $3);
324            $template_text = fill("", "", $presig) . "\n" . $sig;
325        } else {
326            $template_text = fill("", "", $template_text);
327        }
328    }
329    if (defined $control) {
330        $control = join '', map { "Control: $_\n" } @$control;
331    } else {
332        $control = '';
333    }
334    $bugtext = "$type: $package\n$version"
335      . "Severity: $severity\n$tags$user$usertags$control\n$template_text";
336    return $bugtext;
337}
338
339sub div {
340    print +("-" x 79) . "\n";
341}
342
343sub mailbts {
344    my ($subject, $body, $to, $from) = @_;
345
346    if (defined $from) {
347        my $date = strftime "%a, %d %b %Y %T %z", localtime;
348
349        my $pid = open(MAIL, "|-");
350        if (!defined $pid) {
351            die "$progname: Couldn't fork: $!\n";
352        }
353        $SIG{'PIPE'} = sub { die "$progname: pipe for $sendmailcmd broke\n"; };
354        if ($pid) {
355            # parent
356            print MAIL <<"EOM";
357From: $from
358To: $to
359Subject: $subject
360Date: $date
361X-Generator: mass-bug from devscripts ###VERSION###
362
363$body
364EOM
365            close MAIL or die "$progname: sendmail error: $!\n";
366        } else {
367            # child
368            exec(split(' ', $sendmailcmd), "-t")
369              or die "$progname: error running sendmail: $!\n";
370        }
371    } else {    # No $from
372        unless (system("command -v mail >/dev/null 2>&1") == 0) {
373            die
374"$progname: You need to either specify an email address (say using DEBEMAIL)\n or have the mailx/mailutils package installed to send mail!\n";
375        }
376        my $pid = open(MAIL, "|-");
377        if (!defined $pid) {
378            die "$progname: Couldn't fork: $!\n";
379        }
380        $SIG{'PIPE'} = sub { die "$progname: pipe for mail broke\n"; };
381        if ($pid) {
382            # parent
383            print MAIL $body;
384            close MAIL or die "$progname: error running mail: $!\n";
385        } else {
386            # child
387            exec("mail", "-s", $subject, $to)
388              or die "$progname: error running mail: $!\n";
389        }
390    }
391}
392
393my $mode = "display";
394my $subject;
395my $severity = "normal";
396my $tags     = "";
397my $user     = "";
398my $usertags = "";
399my @control  = ();
400my $type     = "Package";
401my $opt_sendmail;
402my $nowrap = "";
403
404if (
405    !GetOptions(
406        "display" => sub { $mode = "display" },
407        "send"    => sub { $mode = "send" },
408        "subject=s"  => \$subject,
409        "severity=s" => \$severity,
410        "tags=s"     => \$tags,
411        "user=s"     => \$user,
412        "usertags=s" => \$usertags,
413        "control=s"  => \@control,
414        "source"     => sub { $type = "Source"; },
415        "sendmail=s" => \$opt_sendmail,
416        "help"       => sub { usage(); exit 0; },
417        "version"    => sub { version(); exit 0; },
418        "no-wrap"    => sub { $nowrap = 1; },
419        'noconf|no-conf' =>
420          sub { die '--noconf must come first on the command line' },
421    )
422) {
423    usageerror();
424}
425
426if (!defined $subject || !length $subject) {
427    print STDERR
428      "$progname: You must specify a subject for the bug reports.\n";
429    usageerror();
430}
431
432unless (
433    $severity =~ /^(wishlist|minor|normal|important|serious|grave|critical)$/)
434{
435    print STDERR
436"$progname: Severity must be one of wishlist, minor, normal, important, serious, grave or critical.\n";
437    usageerror();
438}
439
440if (@ARGV != 2) {
441    usageerror();
442}
443
444if ($tags) {
445    $tags = "Tags: $tags\n";
446}
447
448if ($user) {
449    $user = "User: $user\n";
450}
451
452if ($usertags) {
453    $usertags = "Usertags: $usertags\n";
454}
455
456if ($opt_sendmail) {
457    if (    $opt_sendmail ne '/usr/sbin/sendmail'
458        and $opt_sendmail ne $sendmailcmd) {
459        my $cmd = (split ' ', $opt_sendmail)[0];
460        unless ($cmd =~ /^~?[A-Za-z0-9_\-\+\.\/]*$/) {
461            warn
462"--sendmail command contained funny characters: $cmd\nReverting to default value $sendmailcmd\n";
463            undef $opt_sendmail;
464        } elsif (system("command -v $cmd >/dev/null 2>&1") != 0) {
465            warn
466"--sendmail command $cmd could not be executed.\nReverting to default value $sendmailcmd\n";
467            undef $opt_sendmail;
468        }
469    }
470}
471$sendmailcmd = $opt_sendmail if $opt_sendmail;
472
473my $template     = shift;
474my $package_list = shift;
475
476my $template_text;
477open(T, "$template") || die "$progname: error reading $template: $!\n";
478{
479    local $/ = undef;
480    $template_text = <T>;
481}
482close T;
483if (!length $template_text) {
484    die "$progname: empty template\n";
485}
486
487my @packages;
488open(L, "$package_list") || die "$progname: error reading $package_list: $!\n";
489while (<L>) {
490    chomp;
491    if (!/^([-+\.a-z0-9]+)(?:_(.*))?$/) {
492        die "\"$_\" does not look like the name of a Debian package\n";
493    }
494    push @packages, $1;
495    $versions{$1} = $2 if $2;
496}
497close L;
498
499# Uses variables from above.
500sub showsample {
501    my $package = shift;
502
503    print "To: $submission_email\n";
504    print "Subject: " . gen_subject($subject, $package) . "\n";
505    print "\n";
506    print gen_bug(
507        $template_text, $package, $severity, $tags, $user,
508        $usertags,      $nowrap,  $type,     \@control
509    ) . "\n";
510}
511
512if ($mode eq 'display') {
513    print "Displaying all " . scalar(@packages) . " bug reports..\n";
514    print "Run again with --send switch to send the bug reports.\n";
515    div();
516    foreach my $package (@packages) {
517        showsample($package);
518        div();
519    }
520} elsif ($mode eq 'send') {
521    my $from;
522    $from ||= $ENV{'DEBEMAIL'};
523    $from ||= $ENV{'EMAIL'};
524
525    print "Preparing to send "
526      . scalar(@packages)
527      . " bug reports like this one:\n";
528    div();
529    showsample($packages[0]);
530    div();
531    $| = 1;
532    print
533"Are you sure that you have read the Developer's Reference on mass-filing\nbug reports, have checked this case out on debian-devel, and really want to\nsend out these "
534      . scalar(@packages)
535      . " bug reports? [yes/no] ";
536    my $ans = <STDIN>;
537
538    unless ($ans =~ /^yes$/i) {
539        print "OK, aborting.\n";
540        exit 0;
541    }
542    print "OK, going ahead then...\n";
543    foreach my $package (@packages) {
544        print "Sending bug for $package ...\n";
545        mailbts(
546            gen_subject($subject, $package),
547            gen_bug(
548                $template_text, $package, $severity,
549                $tags,          $user,    $usertags,
550                $nowrap,        $type,    \@control,
551            ),
552            $submission_email,
553            $from
554        );
555    }
556    print "All bugs sent.\n";
557}
558
559=head1 COPYRIGHT
560
561This program is Copyright (C) 2006 by Joey Hess <joeyh@debian.org>.
562
563It is licensed under the terms of the GPL, either version 2 of the
564License, or (at your option) any later version.
565
566=head1 AUTHOR
567
568Joey Hess <joeyh@debian.org>
569
570=cut
571