pjs/webtools/bugzilla/processmail

745 строки
22 KiB
Perl
Executable File

#!/usr/bonsaitools/bin/perl -w
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>,
# Bryce Nesbitt <bryce-mozilla@nextbus.com>
# Dan Mosedale <dmose@mozilla.org>
# To recreate the shadow database, run "processmail regenerate" .
use diagnostics;
use strict;
require "globals.pl";
use RelationSet;
$| = 1;
umask(0);
$::lockcount = 0;
my $regenerate = 0;
my $nametoexclude = "";
my @forcecc;
sub Lock {
if ($::lockcount <= 0) {
$::lockcount = 0;
if (!open(LOCKFID, ">>data/maillock")) {
mkdir "data", 0777;
chmod 0777, "data";
open(LOCKFID, ">>data/maillock") || die "Can't open lockfile.";
}
my $val = flock(LOCKFID,2);
if (!$val) { # '2' is magic 'exclusive lock' const.
print "Lock failed: $val\n";
}
chmod 0666, "data/maillock";
}
$::lockcount++;
}
sub Unlock {
$::lockcount--;
if ($::lockcount <= 0) {
flock(LOCKFID,8); # '8' is magic 'unlock' const.
close LOCKFID;
}
}
sub FileSize {
my ($filename) = (@_);
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
$atime,$mtime,$ctime,$blksize,$blocks)
= stat($filename);
if (defined $size) {
return $size;
}
return -1;
}
sub Different {
my ($file1, $file2) = (@_);
my $size1 = FileSize($file1);
my $size2 = FileSize($file2);
if ($size1 != $size2) {
return 1;
}
open(FID1, "<$file1") || die "Can't open $file1";
open(FID2, "<$file2") || die "Can't open $file2";
my $d1;
my $d2;
if (read(FID1, $d1, $size1) ne $size1) {
die "Can't read $size1 bytes from $file1";
}
if (read(FID2, $d2, $size2) ne $size2) {
die "Can't read $size2 bytes from $file2";
}
close FID1;
close FID2;
return ($d1 ne $d2);
}
sub DescCC {
my $cclist = shift();
return "" if ( $cclist->size() == 0 );
return "Cc: " . $cclist->toString() . "\n";
}
sub DescDependencies {
my ($id) = (@_);
if (!Param("usedependencies")) {
return "";
}
my $result = "";
my $me = "blocked";
my $target = "dependson";
my $title = "BugsThisDependsOn";
for (1..2) {
SendSQL("select $target from dependencies where $me = $id order by $target");
my @list;
while (MoreSQLData()) {
push(@list, FetchOneColumn());
}
if (@list) {
my @verbose;
my $count = 0;
foreach my $i (@list) {
SendSQL("select bug_status, resolution from bugs where bug_id = $i");
my ($bug_status, $resolution) = (FetchSQLData());
my $desc;
if ($bug_status eq "NEW" || $bug_status eq "ASSIGNED" ||
$bug_status eq "REOPENED") {
$desc = "";
} else {
$desc = "[$resolution]";
}
push(@verbose, $i . "$desc");
$count++;
}
if ($count > 5) {
$result .= "$title: Big list (more than 5) has been omitted\n";
} else {
$result .= "$title: " . join(', ', @verbose) . "\n";
}
}
my $tmp = $me;
$me = $target;
$target = $tmp;
$title = "OtherBugsDependingOnThis";
}
return $result;
}
sub GetBugText {
my ($id) = (@_);
undef %::bug;
my @collist = ("bug_id", "product", "version", "rep_platform", "op_sys",
"bug_status", "resolution", "priority", "bug_severity",
"assigned_to", "reporter", "bug_file_loc",
"short_desc", "component", "qa_contact", "target_milestone",
"status_whiteboard", "groupset");
my $query = "select " . join(", ", @collist) .
" from bugs where bug_id = $id";
SendSQL($query);
my @row;
if (!(@row = FetchSQLData())) {
return "";
}
foreach my $field (@collist) {
$::bug{$field} = shift @row;
if (!defined $::bug{$field}) {
$::bug{$field} = "";
}
}
$::bug{'assigned_to'} = DBID_to_name($::bug{'assigned_to'});
$::bug{'reporter'} = DBID_to_name($::bug{'reporter'});
my $qa_contact = "";
my $target_milestone = "";
my $status_whiteboard = "";
if (Param('useqacontact') && $::bug{'qa_contact'} > 0) {
$::bug{'qa_contact'} = DBID_to_name($::bug{'qa_contact'});
$qa_contact = "QAContact: $::bug{'qa_contact'}\n";
} else {
$::bug{'qa_contact'} = "";
}
if (Param('usetargetmilestone') && $::bug{'target_milestone'} ne "") {
$target_milestone = "TargetMilestone: $::bug{'target_milestone'}\n";
}
if (Param('usestatuswhiteboard') && $::bug{'status_whiteboard'} ne "") {
$status_whiteboard = "StatusWhiteboard: $::bug{'status_whiteboard'}\n";
}
$::bug{'long_desc'} = GetLongDescriptionAsText($id);
my $cclist = new RelationSet();
$cclist->mergeFromDB("select who from cc where bug_id = $id");
my @voterlist;
SendSQL("select profiles.login_name from votes, profiles where votes.bug_id = $id and profiles.userid = votes.who");
while (MoreSQLData()) {
my $v = FetchOneColumn();
push(@voterlist, $v);
}
$::bug{'cclist'} = $cclist->toString();
$::bug{'voterlist'} = join(',', @voterlist);
if (Param("prettyasciimail")) {
$^A = "";
my $temp = formline <<'END',$::bug{'short_desc'},$id,$::bug{'product'},$::bug{'bug_status'},$::bug{'version'},$::bug{'resolution'},$::bug{'rep_platform'},$::bug{'bug_severity'},$::bug{'op_sys'},$::bug{'priority'},$::bug{'component'},$::bug{'assigned_to'},$::bug{'reporter'},$qa_contact,DescCC($cclist),$target_milestone,${status_whiteboard},$::bug{'bug_file_loc'},DescDependencies($id);
+============================================================================+
| @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
+----------------------------------------------------------------------------+
| Bug #: @<<<<<<<<<<< Product: @<<<<<<<<<<<<<<<<<<<<<< |
| Status: @<<<<<<<<<<<<<<<<<< Version: @<<<<<<<<<<<<<<<<<<<<<< |
| Resolution: @<<<<<<<<<<<<<<<<<< Platform: @<<<<<<<<<<<<<<<<<<<<<< |
| Severity: @<<<<<<<<<<<<<<<<<< OS/Version: @<<<<<<<<<<<<<<<<<<<<<< |
| Priority: @<<<<<<<<<<<<<<<<<< Component: @<<<<<<<<<<<<<<<<<<<<<< |
+----------------------------------------------------------------------------+
| Assigned To: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
| Reported By: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
| ~QA Contact: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
| ~ CC list: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
+----------------------------------------------------------------------------+
| ~ Milestone: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
|~ Whiteboard: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
| URL: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
|~Dependencies: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
+============================================================================+
| DESCRIPTION |
END
my $prettymail = $^A . $::bug{'long_desc'};
return $prettymail;
} else {
return "Bug\#: $id
Product: $::bug{'product'}
Version: $::bug{'version'}
Platform: $::bug{'rep_platform'}
OS/Version: $::bug{'op_sys'}
Status: $::bug{'bug_status'}
Resolution: $::bug{'resolution'}
Severity: $::bug{'bug_severity'}
Priority: $::bug{'priority'}
Component: $::bug{'component'}
AssignedTo: $::bug{'assigned_to'}
ReportedBy: $::bug{'reporter'}
$qa_contact$target_milestone${status_whiteboard}URL: $::bug{'bug_file_loc'}
" . DescCC($cclist) . "Summary: $::bug{'short_desc'}
" . DescDependencies($id) . "
$::bug{'long_desc'}
";
}
}
my $didexclude = 0;
my %seen;
my @sentlist;
sub fixaddresses {
my ($field, $list) = (@_);
my @result;
foreach my $i (@$list) {
if (!defined $i || $i eq "") {
next;
}
SendSQL("select emailnotification, groupset & $::bug{'groupset'} from profiles where login_name = " .
SqlQuote($i));
my ($emailnotification, $groupset) = (FetchSQLData());
if ($groupset ne $::bug{'groupset'}) {
next;
}
if ($emailnotification eq "CConly") {
if ($field ne "cc") {
next;
}
}
if ($emailnotification eq "ExcludeSelfChanges" &&
(lc($i) eq $nametoexclude)) {
$didexclude = 1;
next;
}
if (!defined $::nomail{$i} && !defined $seen{$i}) {
push(@result, $i . Param('emailsuffix'));
$seen{$i} = 1;
}
}
return join(", ", @result);
}
sub Log {
my ($str) = (@_);
Lock();
open(FID, ">>data/maillog") || die "Can't write to data/maillog";
print FID time2str("%D %H:%M", time()) . ": $str\n";
close FID;
Unlock();
}
sub FormatTriple {
my ($a, $b, $c) = (@_);
$^A = "";
my $temp = formline << 'END', $a, $b, $c;
^>>>>>>>>>>>>>>>>>>|^<<<<<<<<<<<<<<<<<<<<<<<<<<<|^<<<<<<<<<<<<<<<<<<<<<<<<<<~~
END
; # This semicolon appeases my emacs editor macros. :-)
return $^A;
}
sub FormatDouble {
my ($a, $b) = (@_);
$a .= ":";
$^A = "";
my $temp = formline << 'END', $a, $b;
^>>>>>>>>>>>>>>>>>> ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
END
; # This semicolon appeases my emacs editor macros. :-)
return $^A;
}
sub NewProcessOneBug {
my ($id) = (@_);
my @headerlist;
my %values;
my %defmailhead;
my %fielddescription;
my $msg = "";
SendSQL("SELECT name, description, mailhead FROM fielddefs " .
"ORDER BY sortkey");
while (MoreSQLData()) {
my ($field, $description, $mailhead) = (FetchSQLData());
push(@headerlist, $field);
$defmailhead{$field} = $mailhead;
$fielddescription{$field} = $description;
}
SendSQL("SELECT " . join(',', @::log_columns) . ", lastdiffed, now() " .
"FROM bugs WHERE bug_id = $id");
my @row = FetchSQLData();
foreach my $i (@::log_columns) {
$values{$i} = shift(@row);
}
my ($start, $end) = (@row);
my $ccSet = new RelationSet();
$ccSet->mergeFromDB("SELECT who FROM cc WHERE bug_id = $id");
$values{'cc'} = $ccSet->toString();
my @voterlist;
SendSQL("SELECT profiles.login_name FROM votes, profiles " .
"WHERE votes.bug_id = $id AND profiles.userid = votes.who");
while (MoreSQLData()) {
push(@voterlist, FetchOneColumn());
}
$values{'assigned_to'} = DBID_to_name($values{'assigned_to'});
$values{'reporter'} = DBID_to_name($values{'reporter'});
if ($values{'qa_contact'}) {
$values{'qa_contact'} = DBID_to_name($values{'qa_contact'});
}
my @diffs;
SendSQL("SELECT profiles.login_name, fielddefs.description, " .
" bug_when, oldvalue, newvalue " .
"FROM bugs_activity, fielddefs, profiles " .
"WHERE bug_id = $id " .
" AND fielddefs.fieldid = bugs_activity.fieldid " .
" AND profiles.userid = who " .
" AND bug_when > '$start' " .
" AND bug_when <= '$end' " .
"ORDER BY bug_when"
);
while (MoreSQLData()) {
my @row = FetchSQLData();
push(@diffs, \@row);
}
my $difftext = "";
my $lastwho = "";
foreach my $ref (@diffs) {
my ($who, $what, $when, $old, $new) = (@$ref);
if ($who ne $lastwho) {
$lastwho = $who;
$difftext .= "\n$who changed:\n\n";
$difftext .= FormatTriple("What ", "Old Value", "New Value");
$difftext .= ('-' x 76) . "\n";
}
$difftext .= FormatTriple($what, $old, $new);
}
$difftext = trim($difftext);
my $deptext = "";
my $resid =
SendSQL("SELECT bugs_activity.bug_id, fielddefs.name, " .
" oldvalue, newvalue " .
"FROM bugs_activity, dependencies, fielddefs ".
"WHERE bugs_activity.bug_id = dependencies.dependson " .
" AND dependencies.blocked = $id " .
" AND fielddefs.fieldid = bugs_activity.fieldid" .
" AND (fielddefs.name = 'bug_status' " .
" OR fielddefs.name = 'resolution') " .
" AND bug_when > '$start' " .
" AND bug_when <= '$end' " .
"ORDER BY bug_when, bug_id");
my $thisdiff = "";
my $lastbug = "";
my $interestingchange = 0;
while (MoreSQLData()) {
my ($bug, $what, $old, $new) = (FetchSQLData());
if ($bug ne $lastbug) {
if ($interestingchange) {
$deptext .= $thisdiff;
}
$lastbug = $bug;
$thisdiff =
"\nThis bug depends on bug $bug, which changed state:\n\n";
$thisdiff .= FormatTriple("What ", "Old Value", "New Value");
$thisdiff .= ('-' x 76) . "\n";
$interestingchange = 0;
}
$thisdiff .= FormatTriple($fielddescription{$what}, $old, $new);
if ($what eq 'bug_status' && IsOpenedState($old) ne IsOpenedState($new)) {
$interestingchange = 1;
}
}
if ($interestingchange) {
$deptext .= $thisdiff;
}
$deptext = trim($deptext);
if ($deptext) {
$difftext = trim($difftext . "\n\n" . $deptext);
}
my $newcomments = GetLongDescriptionAsText($id, $start, $end);
my $count = 0;
for my $person ($values{'assigned_to'}, $values{'reporter'},
split(/,/, $values{'cc'}),
@voterlist,
@forcecc) {
$count++;
NewProcessOnePerson($person, $count, \@headerlist, \%values,
\%defmailhead, \%fielddescription, $difftext,
$newcomments, $start, $id, 1);
}
SendSQL("UPDATE bugs SET lastdiffed = '$end', delta_ts = delta_ts " .
"WHERE bug_id = $id");
}
sub NewProcessOnePerson ($$\@\%\%\%$$$$) {
my ($person, $count, $hlRef, $valueRef, $dmhRef, $fdRef, $difftext,
$newcomments, $start, $id, $checkWatchers) = @_;
my %values = %$valueRef;
my @headerlist = @$hlRef;
my %defmailhead = %$dmhRef;
my %fielddescription = %$fdRef;
if ($seen{$person}) {
return;
}
SendSQL("SELECT userid, emailnotification, newemailtech," .
" groupset & $values{'groupset'} " .
"FROM profiles WHERE login_name = " . SqlQuote($person));
my ($userid, $emailnotification, $newemailtech,
$groupset) = (FetchSQLData());
# check for watchers, and recurse if we find any, but tell the
# recursive call not to check for watchers
#
if (Param("supportwatchers") && $checkWatchers) {
my $personId = DBname_to_id($person);
my $watcherSet = new RelationSet();
$watcherSet->mergeFromDB("SELECT watcher FROM watch WHERE" .
" watched = $personId");
foreach my $watcher ( $watcherSet->toArray() ) {
NewProcessOnePerson(DBID_to_name($watcher),
$count, \@headerlist, \%values,
\%defmailhead, \%fielddescription, $difftext,
$newcomments, $start, $id, 0);
}
}
if (!$newemailtech || !Param('newemailtech')) {
return;
}
$seen{$person} = 1;
# if this person doesn't have permission to see info on this bug,
# return.
#
# XXX - I _think_ this currently means that if a bug is suddenly given
# more restrictive permissions, people without those permissions won't
# see the action of restricting the bug itself; the bug will just
# quietly disappear from their radar.
#
if ($groupset ne $values{'groupset'}) {
return;
}
if ($emailnotification eq "ExcludeSelfChanges" &&
lc($person) eq $nametoexclude) {
$didexclude = 1;
return;
}
# "$count < 3" means "this person is either assigned_to or reporter"
#
if ($emailnotification eq "CCOnly" && $count < 3) {
return;
}
my %mailhead = %defmailhead;
my $head = "";
foreach my $f (@headerlist) {
if ($mailhead{$f}) {
my $value = $values{$f};
if (!defined $value) {
# Probaby ought to whine or something. ###
next;
}
my $desc = $fielddescription{$f};
$head .= FormatDouble($desc, $value);
}
}
if ($difftext eq "" && $newcomments eq "") {
# Whoops, no differences!
return;
}
my $isnew = ($start !~ m/[1-9]/);
my %substs;
$substs{"neworchanged"} = $isnew ? "New" : "Changed";
$substs{"to"} = $person;
$substs{"cc"} = '';
$substs{"bugid"} = $id;
if ($isnew) {
$substs{"diffs"} = $head . "\n\n" . $newcomments;
} else {
$substs{"diffs"} = $difftext . "\n\n" . $newcomments;
}
$substs{"summary"} = $values{'short_desc'};
my $template = Param("newchangedmail");
my $msg = PerformSubsts($template, \%substs);
open(SENDMAIL, "|/usr/lib/sendmail -ODeliveryMode=deferred -t") ||
die "Can't open sendmail";
print SENDMAIL trim($msg) . "\n";
close SENDMAIL;
push(@sentlist, $person);
}
sub ProcessOneBug {
my $i = $_[0];
NewProcessOneBug($i);
my $old = "shadow/$i";
my $new = "shadow/$i.tmp.$$";
my $diffs = "shadow/$i.diffs.$$";
my $verb = "Changed";
if (!stat($old)) {
mkdir "shadow", 0777;
chmod 0777, "shadow";
open(OLD, ">$old") || die "Couldn't create null $old";
close OLD;
$verb = "New";
}
my $text = GetBugText($i);
if ($text eq "") {
die "Couldn't find bug $i.";
}
open(FID, ">$new") || die "Couldn't create $new";
print FID $text;
close FID;
if (Different($old, $new)) {
system("diff -c -b $old $new > $diffs");
my $tolist = fixaddresses("to",
[$::bug{'assigned_to'}, $::bug{'reporter'},
$::bug{'qa_contact'}]);
my @combinedcc;
foreach my $v (split(/,/, "$::bug{'cclist'},$::bug{'voterlist'}")) {
push @combinedcc, $v;
}
push (@combinedcc, (@forcecc));
my $cclist = fixaddresses("cc", \@combinedcc);
my $logstr = "Bug $i $verb";
if ($tolist ne "" || $cclist ne "") {
my %substs;
$substs{"fullbugreport"} = $text; # added ability to include the full bug report
$substs{"to"} = $tolist;
$substs{"cc"} = $cclist;
$substs{"bugid"} = $i;
$substs{"diffs"} = "";
open(DIFFS, "<$diffs") || die "Can't open $diffs";
while (<DIFFS>) {
$substs{"diffs"} .= $_;
}
close DIFFS;
$substs{"neworchanged"} = $verb;
$substs{"summary"} = $::bug{'short_desc'};
my $msg = PerformSubsts(Param("changedmail"), \%substs);
if (!$regenerate) {
# Note: fixaddresses may result in a Cc: only. This seems
# harmless.
open(SENDMAIL,
"|/usr/lib/sendmail -ODeliveryMode=deferred -t") ||
die "Can't open sendmail";
print SENDMAIL $msg;
close SENDMAIL;
foreach my $n (split(/[, ]+/, "$tolist,$cclist")) {
if ($n ne "") {
push(@sentlist, $n);
}
}
$logstr = "$logstr; mail sent to $tolist, $cclist";
}
}
unlink($diffs);
Log($logstr);
}
if (@sentlist) {
print "<B>Email sent to:</B> " . join(", ", @sentlist) . "\n";
if ($didexclude) {
print qq{<B>Excluding:</B> $nametoexclude (<a href="userprefs.cgi?bank=diffs">change your preferences</a> if you wish not to be excluded)\n};
}
}
rename($new, $old) || die "Can't rename $new to $old";
chmod 0666, $old;
if ($regenerate) {
print "$i ";
}
%seen = ();
@sentlist = ();
}
# Code starts here
ConnectToDatabase();
GetVersionTable();
Lock();
if (open(FID, "<data/nomail")) {
while (<FID>) {
$::nomail{trim($_)} = 1;
}
close FID;
}
# To recreate the shadow database, run "processmail regenerate" .
if ($#ARGV >= 0 && $ARGV[0] eq "regenerate") {
$regenerate = 1;
shift @ARGV;
SendSQL("select bug_id from bugs order by bug_id");
my @regenerate_list;
while (my @row = FetchSQLData()) {
push @regenerate_list, $row[0];
}
foreach my $i (@regenerate_list) {
ProcessOneBug($i);
Unlock();
Lock();
}
print("\n");
exit;
}
if ($#ARGV >= 0 && $ARGV[0] eq "-forcecc") {
shift(@ARGV);
foreach my $i (split(/,/, shift(@ARGV))) {
push(@forcecc, trim($i));
}
}
if (($#ARGV < 0) || ($#ARGV > 1)) {
print "Usage error: processmail {bugid} {nametoexclude}\nOr: processmail regenerate\n";
exit;
}
if ($#ARGV == 1) {
$nametoexclude = lc($ARGV[1]);
}
if ($ARGV[0] eq "rescanall") {
print "<br> Collecting bug ids...\n";
SendSQL("select bug_id from bugs where to_days(now()) - to_days(delta_ts) <= 2 order by bug_id");
my @list;
while (my @row = FetchSQLData()) {
push @list, $row[0];
}
foreach my $id (@list) {
$ARGV[0] = $id;
print "<br> Doing bug $id\n";
ProcessOneBug($ARGV[0]);
}
} else {
ProcessOneBug($ARGV[0]);
}
exit;