From 4a8e3d64a5af8ae7a82cdb7bbbc39afbf38184b4 Mon Sep 17 00:00:00 2001 From: "preed%sigkill.com" <> Date: Mon, 10 Feb 2003 06:04:08 +0000 Subject: Bug 124174 - make processmail a package (Bugzilla::BugMail), r=gerv, r=jth, a=justdave --- Bugzilla/BugMail.pm | 879 ++++++++++++++++++++ Bugzilla/Template.pm | 7 + CGI.pl | 5 +- attachment.cgi | 27 +- post_bug.cgi | 37 +- process_bug.cgi | 48 +- processmail | 928 ---------------------- sanitycheck.cgi | 27 +- template/en/default/attachment/created.html.tmpl | 3 +- template/en/default/attachment/updated.html.tmpl | 3 +- template/en/default/bug/process/bugmail.html.tmpl | 70 ++ template/en/default/bug/process/results.html.tmpl | 4 +- 12 files changed, 1008 insertions(+), 1030 deletions(-) create mode 100644 Bugzilla/BugMail.pm delete mode 100755 processmail create mode 100644 template/en/default/bug/process/bugmail.html.tmpl diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm new file mode 100644 index 000000000..a71effcd2 --- /dev/null +++ b/Bugzilla/BugMail.pm @@ -0,0 +1,879 @@ +# -*- 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 , +# Bryce Nesbitt +# Dan Mosedale +# Alan Raetz +# Jacob Steenhagen +# Matthew Tuck +# Bradley Baetz +# J. Paul Reed + +use strict; + +package Bugzilla::BugMail; + +use RelationSet; + +# This code is really ugly. It was a commandline interface, then it was moved +# There are package-global variables which we rely on ProcessOneBug to clean +# up each time, and other sorts of fun. +# This really needs to be cleaned at some point. + +my $nametoexclude = ""; +my %nomail; +my $last_changed; + +my @excludedAddresses = (); + +# disable email flag for offline debugging work +my $enableSendMail = 1; + +my %force; + +my %seen; +my @sentlist; + +# I got sick of adding &:: to everything. +# However, 'Yuck!' +# I can't require, cause that pulls it in only once, so it won't then be +# in the global package, and these aren't modules, so I can't use globals.pl +# Remove this evilness once our stuff uses real packages. +sub AUTOLOAD { + no strict 'refs'; + use vars qw($AUTOLOAD); + my $subName = $AUTOLOAD; + $subName =~ s/.*::/::/; # remove package name + *$AUTOLOAD = \&$subName; + goto &$AUTOLOAD; +} + +# This is run when we load the package +if (open(NOMAIL, ") { + $nomail{trim($_)} = 1; + } + close(NOMAIL); +} + + +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; +} + +# This is a bit of a hack, basically keeping the old system() +# cmd line interface. Should clean this up at some point. +# +# args: bug_id, and an optional hash ref which may have keys for: +# changer, owner, qa, reporter, cc +# Optional hash contains values of people which will be forced to those +# roles when the email is sent. +# All the names are email addresses, not userids +# values are scalars, except for cc, which is a list +sub Send($;$) { + my ($id, $recipients) = (@_); + + # This doesn't work if its not in a sub. Probably something to do with the + # require abuse we do. + GetVersionTable(); + + # Since any email recipients must be rederived if the user has not + # been rederived since the most recent group change, figure out when that + # is once and determine the need to rederive users using the same DB + # access that gets the user's email address each time a person is + # processed. + SendSQL("SELECT MAX(last_changed) FROM groups"); + ($last_changed) = FetchSQLData(); + + # Make sure to clean up _all_ package vars here. Yuck... + $nametoexclude = $recipients->{'changer'} || ""; + @{$force{'CClist'}} = (exists $recipients->{'cc'} && + scalar($recipients->{'cc'}) > 0) ? map(trim($_), + @{$recipients->{'cc'}}) : (); + @{$force{'Owner'}} = $recipients->{'owner'} ? + (trim($recipients->{'owner'})) : (); + @{$force{'QAcontact'}} = $recipients->{'qacontact'} ? + (trim($recipients->{'qacontact'})) : (); + @{$force{'Reporter'}} = $recipients->{'reporter'} ? + (trim($recipients->{'reporter'})) : (); + @{$force{'Voter'}} = (); + + %seen = (); + @excludedAddresses = (); + @sentlist = (); + + return ProcessOneBug($id); +} + +sub ProcessOneBug($) { + 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); + } + $values{product} = get_product_name($values{product_id}); + $values{component} = get_component_name($values{component_id}); + + my ($start, $end) = (@row); + # $start and $end are considered safe because users can't touch them + trick_taint($start); + trick_taint($end); + + 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'}); + } + $values{'estimated_time'} = FormatTimeUnit($values{'estimated_time'}); + + my @dependslist; + SendSQL("SELECT dependson FROM dependencies WHERE + blocked = $id ORDER BY dependson"); + while (MoreSQLData()) { + push(@dependslist, FetchOneColumn()); + } + $values{'dependson'} = join(",", @dependslist); + + my @blockedlist; + SendSQL("SELECT blocked FROM dependencies WHERE + dependson = $id ORDER BY blocked"); + while (MoreSQLData()) { + push(@blockedlist, FetchOneColumn()); + } + $values{'blocked'} = join(",", @blockedlist); + + my @diffs; + + + SendSQL("SELECT profiles.login_name, fielddefs.description, " . + " bug_when, removed, added, attach_id, fielddefs.name " . + "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 $diffheader = ""; + my $diffpart = {}; + my @diffparts; + my $lastwho = ""; + foreach my $ref (@diffs) { + my ($who, $what, $when, $old, $new, $attachid, $fieldname) = (@$ref); + $diffpart = {}; + if ($who ne $lastwho) { + $lastwho = $who; + $diffheader = "\n$who" . Param('emailsuffix') . " changed:\n\n"; + $diffheader .= FormatTriple("What ", "Removed", "Added"); + $diffheader .= ('-' x 76) . "\n"; + } + $what =~ s/^(Attachment )?/Attachment #$attachid / if $attachid; + if( $fieldname eq 'estimated_time' || + $fieldname eq 'remaining_time' ) { + $old = FormatTimeUnit($old); + $new = FormatTimeUnit($new); + } + $difftext = FormatTriple($what, $old, $new); + $diffpart->{'header'} = $diffheader; + $diffpart->{'fieldname'} = $fieldname; + $diffpart->{'text'} = $difftext; + push(@diffparts, $diffpart); + } + + + my $deptext = ""; + + my $resid = + + SendSQL("SELECT bugs_activity.bug_id, bugs.short_desc, fielddefs.name, " . + " removed, added " . + "FROM bugs_activity, bugs, dependencies, fielddefs ". + "WHERE bugs_activity.bug_id = dependencies.dependson " . + " AND bugs.bug_id = bugs_activity.bug_id ". + " 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; + my $depbug = 0; + my @depbugs; + while (MoreSQLData()) { + my ($summary, $what, $old, $new); + ($depbug, $summary, $what, $old, $new) = (FetchSQLData()); + if ($depbug ne $lastbug) { + if ($interestingchange) { + $deptext .= $thisdiff; + } + $lastbug = $depbug; + my $urlbase = Param("urlbase"); + $thisdiff = + "\nBug $id depends on bug $depbug, which changed state.\n\n" . + "Bug $depbug Summary: $summary\n" . + "${urlbase}show_bug.cgi?id=$depbug\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; + } + + push(@depbugs, $depbug); + } + + if ($interestingchange) { + $deptext .= $thisdiff; + } + + $deptext = trim($deptext); + + if ($deptext) { + #$difftext = trim($difftext . "\n\n" . $deptext); + $diffpart->{'text'} = trim("\n\n" . $deptext); + push(@diffparts, $diffpart); + } + + + my ($newcomments, $anyprivate) = GetLongDescriptionAsText($id, $start, $end); + + # + # Start of email filtering code + # + my $count = 0; + + # Get a list of the reasons a user might receive email about the bug. + my @currentEmailAttributes = + getEmailAttributes(\%values, \@diffs, $newcomments); + + my (@assigned_toList,@reporterList,@qa_contactList,@ccList) = (); + + #open(LOG, ">>/tmp/maillog"); + #print LOG "\nBug ID: $id CurrentEmailAttributes:"; + #print LOG join(',', @currentEmailAttributes) . "\n"; + + @excludedAddresses = (); # zero out global list + + @assigned_toList = filterEmailGroup('Owner', + \@currentEmailAttributes, + $values{'assigned_to'}); + @reporterList = filterEmailGroup('Reporter', + \@currentEmailAttributes, + $values{'reporter'}); + if (Param('useqacontact') && $values{'qa_contact'}) { + @qa_contactList = filterEmailGroup('QAcontact', + \@currentEmailAttributes, + $values{'qa_contact'}); + } else { + @qa_contactList = (); + } + + @ccList = filterEmailGroup('CClist', \@currentEmailAttributes, + $values{'cc'}); + + @voterList = filterEmailGroup('Voter', \@currentEmailAttributes, + join(',',@voterList)); + + my @emailList = (@assigned_toList, @reporterList, + @qa_contactList, @ccList, @voterList); + + # only need one entry per person + my @allEmail = (); + my %AlreadySeen = (); + my $checkperson = ""; + foreach my $person (@emailList) { + # don't modify the original so it sends out with the right case + # based on who came first. + $checkperson = lc($person); + if ( !($AlreadySeen{$checkperson}) ) { + push(@allEmail,$person); + $AlreadySeen{$checkperson}++; + } + } + + #print LOG "\nbug $id email sent: " . join(',', @allEmail) . "\n"; + + @excludedAddresses = filterExcludeList(\@excludedAddresses, + \@allEmail); + + # print LOG "excluded: " . join(',',@excludedAddresses) . "\n\n"; + + foreach my $person ( @allEmail ) { + my @reasons; + + $count++; + + push(@reasons, 'AssignedTo') if lsearch(\@assigned_toList, $person) != -1; + push(@reasons, 'Reporter') if lsearch(\@reporterList, $person) != -1; + push(@reasons, 'QAcontact') if lsearch(\@qa_contactList, $person) != -1; + push(@reasons, 'CC') if lsearch(\@ccList, $person) != -1; + push(@reasons, 'Voter') if lsearch(\@voterList, $person) != -1; + + if ( !defined(NewProcessOnePerson($person, $count, \@headerlist, + \@reasons, \%values, + \%defmailhead, + \%fielddescription, \@diffparts, + $newcomments, + $anyprivate, $start, $id, + \@depbugs))) + { + + # if a value is not returned, this means that the person + # was not sent mail. add them to the excludedAddresses list. + # it will be filtered later for dups. + # + push @excludedAddresses, $person; + + } + } + + + SendSQL("UPDATE bugs SET lastdiffed = '$end', delta_ts = delta_ts " . + "WHERE bug_id = $id"); + + # Filter the exclude list for dupes one last time + @excludedAddresses = filterExcludeList(\@excludedAddresses, + \@sentlist); + + return { sent => \@sentlist, excluded => \@excludedAddresses }; +} + +# When one person is in different fields on one bug, they may be +# excluded from email because of one relationship to the bug (eg +# they're the QA contact) but included because of another (eg they +# also reported the bug). Inclusion takes precedence, so this +# function looks for and removes any users from the exclude list who +# are also on the include list. Additionally, it removes duplicate +# entries from the exclude list. +# +# Arguments are the exclude list and the include list; the cleaned up +# exclude list is returned. +# +sub filterExcludeList ($$) { + + if ($#_ != 1) { + die ("filterExcludeList called with wrong number of args"); + } + + my ($refExcluded, $refAll) = @_; + + my @excludedAddrs = @$refExcluded; + my @allEmail = @$refAll; + my @tmpList = @excludedAddrs; + my (@result,@uniqueResult) = (); + my %alreadySeen; + + foreach my $excluded (@tmpList) { + + push (@result,$excluded); + foreach my $included (@allEmail) { + + # match found, so we remove the entry + if (lc($included) eq lc($excluded)) { + pop(@result); + last; + } + } + } + + # only need one entry per person + my $checkperson = ""; + + foreach my $person (@result) { + $checkperson = lc($person); + if ( !($alreadySeen{$checkperson}) ) { + push(@uniqueResult,$person); + $alreadySeen{$checkperson}++; + } + } + + return @uniqueResult; +} + +# if the Status was changed to Resolved or Verified +# set the Resolved flag +# +# else if Severity, Status, Target Milestone OR Priority fields have any change +# set the Status flag +# +# else if Keywords has changed +# set the Keywords flag +# +# else if CC has changed +# set the CC flag +# +# if the Comments field shows an attachment +# set the Attachment flag +# +# else if Comments exist +# set the Comments flag +# +# if no flags are set and there was some other field change +# set the Status flag +# +sub getEmailAttributes (\%\@$) { + + my ($bug, $fieldDiffs, $commentField) = @_; + my (@flags,@uniqueFlags,%alreadySeen) = (); + + # Add a flag if the status of the bug is "unconfirmed". + if ($bug->{'bug_status'} eq $::unconfirmedstate) { + push (@flags, 'Unconfirmed') + }; + + foreach my $ref (@$fieldDiffs) { + my ($who, $fieldName, $when, $old, $new) = (@$ref); + + #print qq{field: $fieldName $new
}; + + # the STATUS will be flagged for Severity, Status, Target Milestone and + # Priority changes + # + if ( $fieldName eq 'Status' && ($new eq 'RESOLVED' || $new eq 'VERIFIED')) { + push (@flags, 'Resolved'); + } + elsif ( $fieldName eq 'Severity' || $fieldName eq 'Status' || + $fieldName eq 'Priority' || $fieldName eq 'Target Milestone') { + push (@flags, 'Status'); + } elsif ( $fieldName eq 'Keywords') { + push (@flags, 'Keywords'); + } elsif ( $fieldName eq 'CC') { + push (@flags, 'CC'); + } + + # These next few lines are for finding out who's been added + # to the Owner, QA, CC, etc. fields. It does not effect + # the @flags array at all, but is run here because it does + # effect filtering later and we're already in the loop. + if ($fieldName eq 'AssignedTo') { + push (@{$force{'Owner'}}, $new); + } elsif ($fieldName eq 'QAcontact') { + push (@{$force{'QAcontact'}}, $new); + } elsif ($fieldName eq 'CC') { + my @added = split (/[ ,]/, $new); + push (@{$force{'CClist'}}, @added); + } + } + + if ( $commentField =~ /Created an attachment \(/ ) { + push (@flags, 'Attachments'); + } + elsif ( ($commentField ne '') && !(scalar(@flags) == 1 && $flags[0] eq 'Resolved')) { + push (@flags, 'Comments'); + } + + # default fallthrough for any unflagged change is 'Other' + if ( @flags == 0 && @$fieldDiffs != 0 ) { + push (@flags, 'Other'); + } + + # only need one flag per attribute type + foreach my $flag (@flags) { + if ( !($alreadySeen{$flag}) ) { + push(@uniqueFlags,$flag); + $alreadySeen{$flag}++; + } + } + #print "\nEmail Attributes: ", join(' ,',@uniqueFlags), "
\n"; + + # catch-all default, just in case the above logic is faulty + if ( @uniqueFlags == 0) { + push (@uniqueFlags, 'Comments'); + } + + return @uniqueFlags; +} + +sub filterEmailGroup ($$$) { + # This function figures out who should receive email about the bug + # based on the user's role with regard to the bug (assignee, reporter + # etc.), the changes that occurred (new comments, attachment added, + # status changed etc.) and the user's email preferences. + + # Returns a filtered list of those users who would receive email + # about these changes, and adds the names of those users who would + # not receive email about them to the global @excludedAddresses list. + + my ($role, $reasons, $users) = @_; + + # Make a list of users to filter. + my @users = split( /,/ , $users ); + + # Treat users who are in the process of being removed from this role + # as if they were still in it. + push @users, @{$force{$role}}; + + # If this installation supports user watching, add to the list those + # users who are watching other users already on the list. By doing + # this here, we allow watchers to inherit the roles of the people + # they are watching while at the same time filtering email for watchers + # based on their own preferences. + if (Param("supportwatchers") && scalar(@users)) { + # Convert the unfiltered list of users into an SQL-quoted, + # comma-separated string suitable for use in an SQL query. + my $watched = join(",", map(SqlQuote($_), @users)); + SendSQL("SELECT watchers.login_name + FROM watch, profiles AS watchers, profiles AS watched + WHERE watched.login_name IN ($watched) + AND watched.userid = watch.watched + AND watch.watcher = watchers.userid"); + push (@users, FetchOneColumn()) while MoreSQLData(); + } + + # Initialize the list of recipients. + my @recipients = (); + + USER: foreach my $user (@users) { + next unless $user; + + # Get the user's unique ID, and if the user is not registered + # (no idea why unregistered users should even be on this list, + # but the code that was here before I re-wrote it allows this), + # then we do not have any preferences for them, so assume the + # default preference to receive all mail for any reason. + my $userid = DBname_to_id($user); + if (!$userid) { + push(@recipients, $user); + next; + } + + # Get the user's email preferences from the database. + SendSQL("SELECT emailflags FROM profiles WHERE userid = $userid"); + my $prefs = FetchOneColumn(); + + # If the user's preferences are empty, assume the default preference + # to receive all mail. This happens when the installation upgraded + # from a version of Bugzilla without email preferences to one with + # them, but the user has not set their preferences yet. + if (!defined($prefs) || $prefs !~ /email/) { + push(@recipients, $user); + next; + } + + # Write the user's preferences into a Perl record indexed by + # preference name. We pass the value "255" to the split function + # because otherwise split will trim trailing null fields, causing + # Perl to generate lots of warnings. Any suitably large number + # would do. + my %prefs = split(/~/, $prefs, 255); + + # If this user is the one who made the change in the first place, + # and they prefer not to receive mail about their own changes, + # filter them from the list. + if (lc($user) eq $nametoexclude && $prefs{'ExcludeSelf'} eq 'on') { + push(@excludedAddresses, $user); + next; + } + + # If the user doesn't want to receive email about unconfirmed + # bugs, that setting overrides their other preferences, including + # the preference to receive email when they are added to or removed + # from a role, so remove them from the list before checking their + # other preferences. + if (grep(/Unconfirmed/, @$reasons) + && exists($prefs{"email${role}Unconfirmed"}) + && $prefs{"email${role}Unconfirmed"} eq '') + { + push(@excludedAddresses, $user); + next; + } + + # If the user was added to or removed from this role, and they + # prefer to receive email when that happens, send them mail. + # Note: This was originally written to send email when users + # were removed from roles and was later enhanced for additions, + # but for simplicity's sake the name "Removeme" was retained. + if (grep($_ eq $user, @{$force{$role}}) + && $prefs{"email${role}Removeme"} eq 'on') + { + push (@recipients, $user); + next; + } + + # If the user prefers to be included in mail about this change, + # or they haven't specified a preference for it (because they + # haven't visited the email preferences page since the preference + # was added, in which case we include them by default), add them + # to the list of recipients. + foreach my $reason (@$reasons) { + my $pref = "email$role$reason"; + if (!exists($prefs{$pref}) || $prefs{$pref} eq 'on') { + push(@recipients, $user); + next USER; + } + } + + # At this point there's no way the user wants to receive email + # about this change, so exclude them from the list of recipients. + push(@excludedAddresses, $user); + + } # for each user on the unfiltered list + + return @recipients; +} + +sub NewProcessOnePerson ($$$$$$$$$$$$$) { + my ($person, $count, $hlRef, $reasonsRef, $valueRef, $dmhRef, $fdRef, + $diffRef, $newcomments, $anyprivate, $start, + $id, $depbugsRef) = @_; + + my %values = %$valueRef; + my @headerlist = @$hlRef; + my @reasons = @$reasonsRef; + my %defmailhead = %$dmhRef; + my %fielddescription = %$fdRef; + my @diffparts = @$diffRef; + my @depbugs = @$depbugsRef; + + if ($seen{$person}) { + return; + } + + if ($nomail{$person}) { + return; + } + + + SendSQL("SELECT userid, (refreshed_when > " . SqlQuote($last_changed) . + ") FROM profiles WHERE login_name = " . SqlQuote($person)); + my ($userid, $current) = (FetchSQLData()); + + $seen{$person} = 1; + + detaint_natural($userid); + + if (!$current) { + DeriveGroup($userid); + } + + # if this person doesn't have permission to see info on this bug, + # return. + # + # XXX - 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. + # + return unless CanSeeBug($id, $userid); + + + # Drop any non-insiders if the comment is private + return if (Param("insidergroup") && + ($anyprivate != 0) && + (!UserInGroup(Param("insidergroup"), $userid))); + + # We shouldn't send changedmail if this is a dependency mail, and any of + # the depending bugs is not visible to the user. + foreach my $dep_id (@depbugs) { + my $save_id = $dep_id; + detaint_natural($dep_id) || warn("Unexpected Error: \@depbugs contains a non-numeric value: '$save_id'") + && return; + return unless CanSeeBug($dep_id, $userid); + } + + my %mailhead = %defmailhead; + + my $head = ""; + + foreach my $f (@headerlist) { + if ($mailhead{$f}) { + my $value = $values{$f}; + # If there isn't anything to show, don't include this header + if (! $value) { + next; + } + # Don't send estimated_time if user not in the group, or not enabled + if ($f ne 'estimated_time' || + UserInGroup(Param('timetrackinggroup'), $userid)) { + + my $desc = $fielddescription{$f}; + $head .= FormatDouble($desc, $value); + } + } + } + + # Build difftext (the actions) by verifying the user should see them + my $difftext = ""; + my $diffheader = ""; + my $add_diff; + foreach my $diff (@diffparts) { + + $add_diff = 0; + + if ($diff->{'fieldname'} eq 'estimated_time' || + $diff->{'fieldname'} eq 'remaining_time' || + $diff->{'fieldname'} eq 'work_time') { + if (UserInGroup(Param("timetrackinggroup"), $userid)) { + $add_diff = 1; + } + } else { + $add_diff = 1; + } + if ($add_diff) { + if ($diffheader ne $diff->{'header'}) { + $diffheader = $diff->{'header'}; + $difftext .= $diffheader; + } + $difftext .= $diff->{'text'}; + } + } + + if ($difftext eq "" && $newcomments eq "") { + # Whoops, no differences! + return; + } + + my $reasonsbody = "------- You are receiving this mail because: -------\n"; + + if (scalar(@reasons) == 0) { + $reasonsbody .= "Whoops! I have no idea!\n"; + } else { + foreach my $reason (@reasons) { + if ($reason eq 'AssignedTo') { + $reasonsbody .= "You are the assignee for the bug, or are watching the assignee.\n"; + } elsif ($reason eq 'Reporter') { + $reasonsbody .= "You reported the bug, or are watching the reporter.\n"; + } elsif ($reason eq 'QAcontact') { + $reasonsbody .= "You are the QA contact for the bug, or are watching the QA contact.\n"; + } elsif ($reason eq 'CC') { + $reasonsbody .= "You are on the CC list for the bug, or are watching someone who is.\n"; + } elsif ($reason eq 'Voter') { + $reasonsbody .= "You are a voter for the bug, or are watching someone who is.\n"; + } else { + $reasonsbody .= "Whoops! There is an unknown reason!\n"; + } + } + } + + my $isnew = ($start !~ m/[1-9]/); + + my %substs; + + # If an attachment was created, then add an URL. (Note: the 'g'lobal + # replace should work with comments with multiple attachments.) + + if ( $newcomments =~ /Created an attachment \(/ ) { + + my $showattachurlbase = + Param('urlbase') . "attachment.cgi?id="; + + $newcomments =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \(${showattachurlbase}$2&action=view\)/g; + } + + $person .= Param('emailsuffix'); +# 09/13/2000 cyeh@bluemartini.com +# If a bug is changed, don't put the word "Changed" in the subject mail +# since if the bug didn't change, you wouldn't be getting mail +# in the first place! see http://bugzilla.mozilla.org/show_bug.cgi?id=29820 +# for details. + $substs{"neworchanged"} = $isnew ? 'New: ' : ''; + $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'}; + $substs{"reasonsheader"} = join(" ", @reasons); + $substs{"reasonsbody"} = $reasonsbody; + + my $template = Param("newchangedmail"); + + my $msg = PerformSubsts($template, \%substs); + + my $sendmailparam = "-ODeliveryMode=deferred"; + if (Param("sendmailnow")) { + $sendmailparam = ""; + } + + if ($enableSendMail == 1) { + open(SENDMAIL, "|/usr/lib/sendmail $sendmailparam -t -i") || + die "Can't open sendmail"; + + print SENDMAIL trim($msg) . "\n"; + close SENDMAIL; + } + push(@sentlist, $person); + return 1; +} + +1; diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 434785332..463247c86 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -259,6 +259,13 @@ sub create { # UserInGroup - you probably want to cache this 'UserInGroup' => \&::UserInGroup, + # SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm + 'SendBugMail' => sub { + my ($id, $mailrecipients) = (@_); + require Bugzilla::BugMail; + Bugzilla::BugMail::Send($id, $mailrecipients); + }, + # SyncAnyPendingShadowChanges # - called in the footer to sync the shadowdb 'SyncAnyPendingShadowChanges' => \&::SyncAnyPendingShadowChanges, diff --git a/CGI.pl b/CGI.pl index 7ded11ef2..eb74862fa 100644 --- a/CGI.pl +++ b/CGI.pl @@ -755,10 +755,7 @@ sub CheckIfVotedConfirmed { $vars->{'type'} = "votes"; $vars->{'id'} = $id; - $vars->{'mail'} = ""; - open(PMAIL, "-|") or exec('./processmail', $id); - $vars->{'mail'} .= $_ while ; - close(PMAIL); + $vars->{'mailrecipients'} = { 'changer' => $who }; $template->process("bug/process/results.html.tmpl", $vars) || ThrowTemplateError($template->error()); diff --git a/attachment.cgi b/attachment.cgi index bb166e2f8..81037a723 100755 --- a/attachment.cgi +++ b/attachment.cgi @@ -574,23 +574,11 @@ sub insert } } - # Send mail to let people know the attachment has been created. Uses a - # special syntax of the "open" and "exec" commands to capture the output of - # "processmail", which "system" doesn't allow, without running the command - # through a shell, which backticks (``) do. - #system ("./processmail", $bugid , $::userid); - #my $mailresults = `./processmail $bugid $::userid`; - my $mailresults = ''; - open(PMAIL, "-|") or exec('./processmail', '-forcecc', $forcecc, - $::FORM{'bugid'}, $::COOKIE{'Bugzilla_login'}); - $mailresults .= $_ while ; - close(PMAIL); - # Define the variables and functions that will be passed to the UI template. + $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} }; $vars->{'bugid'} = $::FORM{'bugid'}; $vars->{'attachid'} = $attachid; $vars->{'description'} = $description; - $vars->{'mailresults'} = $mailresults; $vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'}; $vars->{'contenttype'} = $::FORM{'contenttype'}; @@ -791,21 +779,10 @@ sub update } - # Send mail to let people know the bug has changed. Uses a special syntax - # of the "open" and "exec" commands to capture the output of "processmail", - # which "system" doesn't allow, without running the command through a shell, - # which backticks (``) do. - #system ("./processmail", $bugid , $::userid); - #my $mailresults = `./processmail $bugid $::userid`; - my $mailresults = ''; - open(PMAIL, "-|") or exec('./processmail', $bugid, DBID_to_name($::userid)); - $mailresults .= $_ while ; - close(PMAIL); - # Define the variables and functions that will be passed to the UI template. + $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} }; $vars->{'attachid'} = $::FORM{'id'}; $vars->{'bugid'} = $bugid; - $vars->{'mailresults'} = $mailresults; # Return the appropriate HTTP response headers. print "Content-Type: text/html\n\n"; diff --git a/post_bug.cgi b/post_bug.cgi index 18b761744..2a2bcb5fa 100755 --- a/post_bug.cgi +++ b/post_bug.cgi @@ -457,30 +457,16 @@ if (UserInGroup("editbugs")) { } } -# Assemble the -force* strings so this counts as "Added to this capacity" -my @ARGLIST = (); -if (@cc) { - push (@ARGLIST, "-forcecc", join(",", @cc)); -} - -push (@ARGLIST, "-forceowner", DBID_to_name($::FORM{assigned_to})); +# Email everyone the details of the new bug +$vars->{'mailrecipients'} = { 'cc' => \@cc, + 'owner' => DBID_to_name($::FORM{'assigned_to'}), + 'reporter' => $::COOKIE{'Bugzilla_login'}, + 'changer' => $::COOKIE{'Bugzilla_login'} }; if (defined $::FORM{'qa_contact'}) { - push (@ARGLIST, "-forceqacontact", DBID_to_name($::FORM{'qa_contact'})); + $vars->{'mailrecipients'}->{'qa'} = DBID_to_name($::FORM{'qa_contact'}); } -push (@ARGLIST, "-forcereporter", DBID_to_name($::userid)); - -push (@ARGLIST, $id, $::COOKIE{'Bugzilla_login'}); - -# Send mail to let people know the bug has been created. -# See attachment.cgi for explanation of why it's done this way. -my $mailresults = ''; -open(PMAIL, "-|") or exec('./processmail', @ARGLIST); -$mailresults .= $_ while ; -close(PMAIL); - -# Tell the user all about it $vars->{'id'} = $id; my $bug = new Bug($id, $::userid); $vars->{'bug'} = $bug; @@ -491,19 +477,10 @@ $vars->{'sentmail'} = []; push (@{$vars->{'sentmail'}}, { type => 'created', id => $id, - mail => $mailresults }); foreach my $i (@all_deps) { - my $mail = ""; - open(PMAIL, "-|") or exec('./processmail', $i, $::COOKIE{'Bugzilla_login'}); - $mail .= $_ while ; - close(PMAIL); - - push (@{$vars->{'sentmail'}}, { type => 'dep', - id => $i, - mail => $mail - }); + push (@{$vars->{'sentmail'}}, { type => 'dep', id => $i, }); } my @bug_list; diff --git a/process_bug.cgi b/process_bug.cgi index 6ca220377..cce3792ee 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -1360,8 +1360,8 @@ foreach my $id (@idlist) { LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames, $whoid, $timestamp); $bug_changed = 1; - - my $removedCcString = ""; + + my @ccRemoved = (); if (defined $::FORM{newcc} || defined $::FORM{removecc} || defined $::FORM{masscc}) { # Get the current CC list for this bug my %oncc; @@ -1387,8 +1387,6 @@ foreach my $id (@idlist) { $oncc{$pid} = 0; } } - # Save off the removedCcString so it can be fed to processmail - $removedCcString = join (",", @removed); # If any changes were found, record it in the activity log if (scalar(@removed) || scalar(@added)) { @@ -1397,6 +1395,7 @@ foreach my $id (@idlist) { LogActivityEntry($id,"cc",$removed,$added,$whoid,$timestamp); $bug_changed = 1; } + @ccRemoved = @removed; } # We need to run processmail for dependson/blocked bugs if the dependencies @@ -1660,28 +1659,10 @@ foreach my $id (@idlist) { } SendSQL("UNLOCK TABLES"); - my @ARGLIST = (); - if ( $removedCcString ne "" ) { - push @ARGLIST, ("-forcecc", $removedCcString); - } - if ( $origOwner ne "" ) { - push @ARGLIST, ("-forceowner", $origOwner); - } - if ( $origQaContact ne "") { - push @ARGLIST, ( "-forceqacontact", $origQaContact); - } - push @ARGLIST, ($id, $::COOKIE{'Bugzilla_login'}); - - # Send mail to let people know the bug has been changed. Uses - # a special syntax of the "open" and "exec" commands to capture - # the output "processmail", which "system" doesn't allow - # (i.e. "system ('./processmail', $bugid , $::userid);"), without - # the insecurity of running the command through a shell via backticks - # (i.e. "my $mailresults = `./processmail $bugid $::userid`;"). - $vars->{'mail'} = ""; - open(PMAIL, "-|") or exec('./processmail', @ARGLIST); - $vars->{'mail'} .= $_ while ; - close(PMAIL); + $vars->{'mailrecipients'} = { 'cc' => \@ccRemoved, + 'owner' => $origOwner, + 'qa' => $origQaContact, + 'changer' => $::COOKIE{'Bugzilla_login'} }; $vars->{'id'} = $id; @@ -1710,11 +1691,9 @@ foreach my $id (@idlist) { CheckFormFieldDefined(\%::FORM,'comment'); SendSQL("INSERT INTO duplicates VALUES ($duplicate, $::FORM{'id'})"); - $vars->{'mail'} = ""; - open(PMAIL, "-|") or exec('./processmail', $duplicate, $::COOKIE{'Bugzilla_login'}); - $vars->{'mail'} .= $_ while ; - close(PMAIL); - + $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} + }; + $vars->{'id'} = $duplicate; $vars->{'type'} = "dupe"; @@ -1725,12 +1704,7 @@ foreach my $id (@idlist) { if ($check_dep_bugs) { foreach my $k (keys(%dependencychanged)) { - $vars->{'mail'} = ""; - open(PMAIL, "-|") - or exec('./processmail', $k, $::COOKIE{'Bugzilla_login'}); - $vars->{'mail'} .= $_ while ; - close(PMAIL); - + $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} }; $vars->{'id'} = $k; $vars->{'type'} = "dep"; diff --git a/processmail b/processmail deleted file mode 100755 index aa3628abd..000000000 --- a/processmail +++ /dev/null @@ -1,928 +0,0 @@ -#!/usr/bonsaitools/bin/perl -wT -# -*- 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 , -# Bryce Nesbitt -# Dan Mosedale -# Alan Raetz -# Jacob Steenhagen -# Matthew Tuck - -use strict; -use lib "."; - -require "globals.pl"; - -use RelationSet; - - -# Shut up misguided -w warnings about "used only once". -sub processmail_sillyness { - my $zz; - $zz = $::unconfirmedstate; -} - -$| = 1; - -umask(0); - -my $nametoexclude = ""; -my %nomail; - -my @excludedAddresses = (); - -# disable email flag for offline debugging work -my $enableSendMail = 1; - -my %force; -@{$force{'QAcontact'}} = (); -@{$force{'Owner'}} = (); -@{$force{'Reporter'}} = (); -@{$force{'CClist'}} = (); -@{$force{'Voter'}} = (); - - -my %seen; -my @sentlist; - -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 ProcessOneBug { - 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); - } - $values{product} = get_product_name($values{product_id}); - $values{component} = get_component_name($values{component_id}); - - my ($start, $end) = (@row); - # $start and $end are considered safe because users can't touch them - trick_taint($start); - trick_taint($end); - - 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'}); - } - $values{'estimated_time'} = FormatTimeUnit($values{'estimated_time'}); - - my @dependslist; - SendSQL("SELECT dependson FROM dependencies WHERE - blocked = $id ORDER BY dependson"); - while (MoreSQLData()) { - push(@dependslist, FetchOneColumn()); - } - $values{'dependson'} = join(",", @dependslist); - - my @blockedlist; - SendSQL("SELECT blocked FROM dependencies WHERE - dependson = $id ORDER BY blocked"); - while (MoreSQLData()) { - push(@blockedlist, FetchOneColumn()); - } - $values{'blocked'} = join(",", @blockedlist); - - my @diffs; - - - SendSQL("SELECT profiles.login_name, fielddefs.description, " . - " bug_when, removed, added, attach_id, fielddefs.name " . - "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 $diffheader = ""; - my $diffpart = {}; - my @diffparts; - my $lastwho = ""; - foreach my $ref (@diffs) { - my ($who, $what, $when, $old, $new, $attachid, $fieldname) = (@$ref); - $diffpart = {}; - if ($who ne $lastwho) { - $lastwho = $who; - $diffheader = "\n$who" . Param('emailsuffix') . " changed:\n\n"; - $diffheader .= FormatTriple("What ", "Removed", "Added"); - $diffheader .= ('-' x 76) . "\n"; - } - $what =~ s/^(Attachment )?/Attachment #$attachid / if $attachid; - if( $fieldname eq 'estimated_time' || - $fieldname eq 'remaining_time' ) { - $old = FormatTimeUnit($old); - $new = FormatTimeUnit($new); - } - $difftext = FormatTriple($what, $old, $new); - $diffpart->{'header'} = $diffheader; - $diffpart->{'fieldname'} = $fieldname; - $diffpart->{'text'} = $difftext; - push(@diffparts, $diffpart); - } - - - my $deptext = ""; - - my $resid = - - SendSQL("SELECT bugs_activity.bug_id, bugs.short_desc, fielddefs.name, " . - " removed, added " . - "FROM bugs_activity, bugs, dependencies, fielddefs ". - "WHERE bugs_activity.bug_id = dependencies.dependson " . - " AND bugs.bug_id = bugs_activity.bug_id ". - " 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; - my $depbug = 0; - my @depbugs; - while (MoreSQLData()) { - my ($summary, $what, $old, $new); - ($depbug, $summary, $what, $old, $new) = (FetchSQLData()); - if ($depbug ne $lastbug) { - if ($interestingchange) { - $deptext .= $thisdiff; - } - $lastbug = $depbug; - my $urlbase = Param("urlbase"); - $thisdiff = - "\nBug $id depends on bug $depbug, which changed state.\n\n" . - "Bug $depbug Summary: $summary\n" . - "${urlbase}show_bug.cgi?id=$depbug\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; - } - - push(@depbugs, $depbug); - } - - if ($interestingchange) { - $deptext .= $thisdiff; - } - - $deptext = trim($deptext); - - if ($deptext) { - #$difftext = trim($difftext . "\n\n" . $deptext); - $diffpart->{'text'} = trim("\n\n" . $deptext); - push(@diffparts, $diffpart); - } - - - my ($newcomments, $anyprivate) = GetLongDescriptionAsText($id, $start, $end); - - # - # Start of email filtering code - # - my $count = 0; - - # Get a list of the reasons a user might receive email about the bug. - my @currentEmailAttributes = - getEmailAttributes(\%values, \@diffs, $newcomments); - - my (@assigned_toList,@reporterList,@qa_contactList,@ccList) = (); - - #open(LOG, ">>/tmp/maillog"); - #print LOG "\nBug ID: $id CurrentEmailAttributes:"; - #print LOG join(',', @currentEmailAttributes) . "\n"; - - @excludedAddresses = (); # zero out global list - - @assigned_toList = filterEmailGroup('Owner', - \@currentEmailAttributes, - $values{'assigned_to'}); - @reporterList = filterEmailGroup('Reporter', - \@currentEmailAttributes, - $values{'reporter'}); - if (Param('useqacontact') && $values{'qa_contact'}) { - @qa_contactList = filterEmailGroup('QAcontact', - \@currentEmailAttributes, - $values{'qa_contact'}); - } else { - @qa_contactList = (); - } - - @ccList = filterEmailGroup('CClist', \@currentEmailAttributes, - $values{'cc'}); - - @voterList = filterEmailGroup('Voter', \@currentEmailAttributes, - join(',',@voterList)); - - my @emailList = (@assigned_toList, @reporterList, - @qa_contactList, @ccList, @voterList); - - # only need one entry per person - my @allEmail = (); - my %AlreadySeen = (); - my $checkperson = ""; - foreach my $person (@emailList) { - # don't modify the original so it sends out with the right case - # based on who came first. - $checkperson = lc($person); - if ( !($AlreadySeen{$checkperson}) ) { - push(@allEmail,$person); - $AlreadySeen{$checkperson}++; - } - } - - #print LOG "\nbug $id email sent: " . join(',', @allEmail) . "\n"; - - @excludedAddresses = filterExcludeList(\@excludedAddresses, - \@allEmail); - - # print LOG "excluded: " . join(',',@excludedAddresses) . "\n\n"; - - foreach my $person ( @allEmail ) { - my @reasons; - - $count++; - - push(@reasons, 'AssignedTo') if lsearch(\@assigned_toList, $person) != -1; - push(@reasons, 'Reporter') if lsearch(\@reporterList, $person) != -1; - push(@reasons, 'QAcontact') if lsearch(\@qa_contactList, $person) != -1; - push(@reasons, 'CC') if lsearch(\@ccList, $person) != -1; - push(@reasons, 'Voter') if lsearch(\@voterList, $person) != -1; - - if ( !defined(NewProcessOnePerson($person, $count, \@headerlist, - \@reasons, \%values, - \%defmailhead, - \%fielddescription, \@diffparts, - $newcomments, - $anyprivate, $start, $id, - \@depbugs))) - { - - # if a value is not returned, this means that the person - # was not sent mail. add them to the excludedAddresses list. - # it will be filtered later for dups. - # - push @excludedAddresses, $person; - - } - } - - - SendSQL("UPDATE bugs SET lastdiffed = '$end', delta_ts = delta_ts " . - "WHERE bug_id = $id"); - - # Filter the exclude list for dupes one last time - @excludedAddresses = filterExcludeList(\@excludedAddresses, - \@sentlist); - if (@sentlist) { - print "Email sent to: " . join(", ", @sentlist) ."
\n"; - } else { - print "Email sent to: no one
\n"; - } - - if (@excludedAddresses) { - print "Excluding: " . join(", ", @excludedAddresses) . "\n"; - } - - print "
If you wish to tweak the kinds of mail Bugzilla sends you, you can"; - print " change your preferences
\n"; - -} - -# When one person is in different fields on one bug, they may be -# excluded from email because of one relationship to the bug (eg -# they're the QA contact) but included because of another (eg they -# also reported the bug). Inclusion takes precedence, so this -# function looks for and removes any users from the exclude list who -# are also on the include list. Additionally, it removes duplicate -# entries from the exclude list. -# -# Arguments are the exclude list and the include list; the cleaned up -# exclude list is returned. -# -sub filterExcludeList ($$) { - - if ($#_ != 1) { - die ("filterExcludeList called with wrong number of args"); - } - - my ($refExcluded, $refAll) = @_; - - my @excludedAddrs = @$refExcluded; - my @allEmail = @$refAll; - my @tmpList = @excludedAddrs; - my (@result,@uniqueResult) = (); - my %alreadySeen; - - foreach my $excluded (@tmpList) { - - push (@result,$excluded); - foreach my $included (@allEmail) { - - # match found, so we remove the entry - if (lc($included) eq lc($excluded)) { - pop(@result); - last; - } - } - } - - # only need one entry per person - my $checkperson = ""; - - foreach my $person (@result) { - $checkperson = lc($person); - if ( !($alreadySeen{$checkperson}) ) { - push(@uniqueResult,$person); - $alreadySeen{$checkperson}++; - } - } - - return @uniqueResult; -} - -# if the Status was changed to Resolved or Verified -# set the Resolved flag -# -# else if Severity, Status, Target Milestone OR Priority fields have any change -# set the Status flag -# -# else if Keywords has changed -# set the Keywords flag -# -# else if CC has changed -# set the CC flag -# -# if the Comments field shows an attachment -# set the Attachment flag -# -# else if Comments exist -# set the Comments flag -# -# if no flags are set and there was some other field change -# set the Status flag -# -sub getEmailAttributes (\%\@$) { - - my ($bug, $fieldDiffs, $commentField) = @_; - my (@flags,@uniqueFlags,%alreadySeen) = (); - - # Add a flag if the status of the bug is "unconfirmed". - if ($bug->{'bug_status'} eq $::unconfirmedstate) { - push (@flags, 'Unconfirmed') - }; - - foreach my $ref (@$fieldDiffs) { - my ($who, $fieldName, $when, $old, $new) = (@$ref); - - #print qq{field: $fieldName $new
}; - - # the STATUS will be flagged for Severity, Status, Target Milestone and - # Priority changes - # - if ( $fieldName eq 'Status' && ($new eq 'RESOLVED' || $new eq 'VERIFIED')) { - push (@flags, 'Resolved'); - } - elsif ( $fieldName eq 'Severity' || $fieldName eq 'Status' || - $fieldName eq 'Priority' || $fieldName eq 'Target Milestone') { - push (@flags, 'Status'); - } elsif ( $fieldName eq 'Keywords') { - push (@flags, 'Keywords'); - } elsif ( $fieldName eq 'CC') { - push (@flags, 'CC'); - } - - # These next few lines are for finding out who's been added - # to the Owner, QA, CC, etc. fields. It does not effect - # the @flags array at all, but is run here because it does - # effect filtering later and we're already in the loop. - if ($fieldName eq 'AssignedTo') { - push (@{$force{'Owner'}}, $new); - } elsif ($fieldName eq 'QAContact') { - push (@{$force{'QAcontact'}}, $new); - } elsif ($fieldName eq 'CC') { - my @added = split (/[ ,]/, $new); - push (@{$force{'CClist'}}, @added); - } - } - - if ( $commentField =~ /Created an attachment \(/ ) { - push (@flags, 'Attachments'); - } - elsif ( ($commentField ne '') && !(scalar(@flags) == 1 && $flags[0] eq 'Resolved')) { - push (@flags, 'Comments'); - } - - # default fallthrough for any unflagged change is 'Other' - if ( @flags == 0 && @$fieldDiffs != 0 ) { - push (@flags, 'Other'); - } - - # only need one flag per attribute type - foreach my $flag (@flags) { - if ( !($alreadySeen{$flag}) ) { - push(@uniqueFlags,$flag); - $alreadySeen{$flag}++; - } - } - #print "\nEmail Attributes: ", join(' ,',@uniqueFlags), "
\n"; - - # catch-all default, just in case the above logic is faulty - if ( @uniqueFlags == 0) { - push (@uniqueFlags, 'Comments'); - } - - return @uniqueFlags; -} - -sub filterEmailGroup ($$$) { - # This function figures out who should receive email about the bug - # based on the user's role with regard to the bug (assignee, reporter - # etc.), the changes that occurred (new comments, attachment added, - # status changed etc.) and the user's email preferences. - - # Returns a filtered list of those users who would receive email - # about these changes, and adds the names of those users who would - # not receive email about them to the global @excludedAddresses list. - - my ($role, $reasons, $users) = @_; - - # Make a list of users to filter. - my @users = split( /,/ , $users ); - - # Treat users who are in the process of being removed from this role - # as if they were still in it. - push @users, @{$force{$role}}; - - # If this installation supports user watching, add to the list those - # users who are watching other users already on the list. By doing - # this here, we allow watchers to inherit the roles of the people - # they are watching while at the same time filtering email for watchers - # based on their own preferences. - if (Param("supportwatchers") && scalar(@users)) { - # Convert the unfiltered list of users into an SQL-quoted, - # comma-separated string suitable for use in an SQL query. - my $watched = join(",", map(SqlQuote($_), @users)); - SendSQL("SELECT watchers.login_name - FROM watch, profiles AS watchers, profiles AS watched - WHERE watched.login_name IN ($watched) - AND watched.userid = watch.watched - AND watch.watcher = watchers.userid"); - push (@users, FetchOneColumn()) while MoreSQLData(); - } - - # Initialize the list of recipients. - my @recipients = (); - - USER: foreach my $user (@users) { - next unless $user; - - # Get the user's unique ID, and if the user is not registered - # (no idea why unregistered users should even be on this list, - # but the code that was here before I re-wrote it allows this), - # then we do not have any preferences for them, so assume the - # default preference to receive all mail for any reason. - my $userid = DBname_to_id($user); - if (!$userid) { - push(@recipients, $user); - next; - } - - # Get the user's email preferences from the database. - SendSQL("SELECT emailflags FROM profiles WHERE userid = $userid"); - my $prefs = FetchOneColumn(); - - # If the user's preferences are empty, assume the default preference - # to receive all mail. This happens when the installation upgraded - # from a version of Bugzilla without email preferences to one with - # them, but the user has not set their preferences yet. - if (!defined($prefs) || $prefs !~ /email/) { - push(@recipients, $user); - next; - } - - # Write the user's preferences into a Perl record indexed by - # preference name. We pass the value "255" to the split function - # because otherwise split will trim trailing null fields, causing - # Perl to generate lots of warnings. Any suitably large number - # would do. - my %prefs = split(/~/, $prefs, 255); - - # If this user is the one who made the change in the first place, - # and they prefer not to receive mail about their own changes, - # filter them from the list. - if (lc($user) eq $nametoexclude && $prefs{'ExcludeSelf'} eq 'on') { - push(@excludedAddresses, $user); - next; - } - - # If the user doesn't want to receive email about unconfirmed - # bugs, that setting overrides their other preferences, including - # the preference to receive email when they are added to or removed - # from a role, so remove them from the list before checking their - # other preferences. - if (grep(/Unconfirmed/, @$reasons) - && exists($prefs{"email${role}Unconfirmed"}) - && $prefs{"email${role}Unconfirmed"} eq '') - { - push(@excludedAddresses, $user); - next; - } - - # If the user was added to or removed from this role, and they - # prefer to receive email when that happens, send them mail. - # Note: This was originally written to send email when users - # were removed from roles and was later enhanced for additions, - # but for simplicity's sake the name "Removeme" was retained. - if (grep($_ eq $user, @{$force{$role}}) - && $prefs{"email${role}Removeme"} eq 'on') - { - push (@recipients, $user); - next; - } - - # If the user prefers to be included in mail about this change, - # or they haven't specified a preference for it (because they - # haven't visited the email preferences page since the preference - # was added, in which case we include them by default), add them - # to the list of recipients. - foreach my $reason (@$reasons) { - my $pref = "email$role$reason"; - if (!exists($prefs{$pref}) || $prefs{$pref} eq 'on') { - push(@recipients, $user); - next USER; - } - } - - # At this point there's no way the user wants to receive email - # about this change, so exclude them from the list of recipients. - push(@excludedAddresses, $user); - - } # for each user on the unfiltered list - - return @recipients; -} - -sub NewProcessOnePerson ($$$$$$$$$$$$$) { - my ($person, $count, $hlRef, $reasonsRef, $valueRef, $dmhRef, $fdRef, - $diffRef, $newcomments, $anyprivate, $start, - $id, $depbugsRef) = @_; - - my %values = %$valueRef; - my @headerlist = @$hlRef; - my @reasons = @$reasonsRef; - my %defmailhead = %$dmhRef; - my %fielddescription = %$fdRef; - my @diffparts = @$diffRef; - my @depbugs = @$depbugsRef; - - if ($seen{$person}) { - return; - } - - if ($nomail{$person}) { - return; - } - - - SendSQL("SELECT userid, (refreshed_when > " . SqlQuote($::last_changed) . ") " . - "FROM profiles WHERE login_name = " . SqlQuote($person)); - my ($userid, $current) = (FetchSQLData()); - - $seen{$person} = 1; - - detaint_natural($userid); - - if (!$current) { - DeriveGroup($userid); - } - - # if this person doesn't have permission to see info on this bug, - # return. - # - # XXX - 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. - # - return unless CanSeeBug($id, $userid); - - - # Drop any non-insiders if the comment is private - return if (Param("insidergroup") && - ($anyprivate != 0) && - (!UserInGroup(Param("insidergroup"), $userid))); - - # We shouldn't send changedmail if this is a dependency mail, and any of - # the depending bugs is not visible to the user. - foreach my $dep_id (@depbugs) { - my $save_id = $dep_id; - detaint_natural($dep_id) || warn("Unexpected Error: \@depbugs contains a non-numeric value: '$save_id'") - && return; - return unless CanSeeBug($dep_id, $userid); - } - - my %mailhead = %defmailhead; - - my $head = ""; - - foreach my $f (@headerlist) { - if ($mailhead{$f}) { - my $value = $values{$f}; - # If there isn't anything to show, don't include this header - if (! $value) { - next; - } - # Don't send estimated_time if user not in the group, or not enabled - if ($f ne 'estimated_time' || - UserInGroup(Param('timetrackinggroup'), $userid)) { - - my $desc = $fielddescription{$f}; - $head .= FormatDouble($desc, $value); - } - } - } - - # Build difftext (the actions) by verifying the user should see them - my $difftext = ""; - my $diffheader = ""; - my $add_diff; - foreach my $diff (@diffparts) { - - $add_diff = 0; - - if ($diff->{'fieldname'} eq 'estimated_time' || - $diff->{'fieldname'} eq 'remaining_time' || - $diff->{'fieldname'} eq 'work_time') { - if (UserInGroup(Param("timetrackinggroup"), $userid)) { - $add_diff = 1; - } - } else { - $add_diff = 1; - } - if ($add_diff) { - if ($diffheader ne $diff->{'header'}) { - $diffheader = $diff->{'header'}; - $difftext .= $diffheader; - } - $difftext .= $diff->{'text'}; - } - } - - if ($difftext eq "" && $newcomments eq "") { - # Whoops, no differences! - return; - } - - my $reasonsbody = "------- You are receiving this mail because: -------\n"; - - if (scalar(@reasons) == 0) { - $reasonsbody .= "Whoops! I have no idea!\n"; - } else { - foreach my $reason (@reasons) { - if ($reason eq 'AssignedTo') { - $reasonsbody .= "You are the assignee for the bug, or are watching the assignee.\n"; - } elsif ($reason eq 'Reporter') { - $reasonsbody .= "You reported the bug, or are watching the reporter.\n"; - } elsif ($reason eq 'QAcontact') { - $reasonsbody .= "You are the QA contact for the bug, or are watching the QA contact.\n"; - } elsif ($reason eq 'CC') { - $reasonsbody .= "You are on the CC list for the bug, or are watching someone who is.\n"; - } elsif ($reason eq 'Voter') { - $reasonsbody .= "You are a voter for the bug, or are watching someone who is.\n"; - } else { - $reasonsbody .= "Whoops! There is an unknown reason!\n"; - } - } - } - - my $isnew = ($start !~ m/[1-9]/); - - my %substs; - - # If an attachment was created, then add an URL. (Note: the 'g'lobal - # replace should work with comments with multiple attachments.) - - if ( $newcomments =~ /Created an attachment \(/ ) { - - my $showattachurlbase = - Param('urlbase') . "attachment.cgi?id="; - - $newcomments =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \(${showattachurlbase}$2&action=view\)/g; - } - - $person .= Param('emailsuffix'); -# 09/13/2000 cyeh@bluemartini.com -# If a bug is changed, don't put the word "Changed" in the subject mail -# since if the bug didn't change, you wouldn't be getting mail -# in the first place! see http://bugzilla.mozilla.org/show_bug.cgi?id=29820 -# for details. - $substs{"neworchanged"} = $isnew ? 'New: ' : ''; - $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'}; - $substs{"reasonsheader"} = join(" ", @reasons); - $substs{"reasonsbody"} = $reasonsbody; - - my $template = Param("newchangedmail"); - - my $msg = PerformSubsts($template, \%substs); - - my $sendmailparam = "-ODeliveryMode=deferred"; - if (Param("sendmailnow")) { - $sendmailparam = ""; - } - - if ($enableSendMail == 1) { - open(SENDMAIL, "|/usr/lib/sendmail $sendmailparam -t -i") || - die "Can't open sendmail"; - - print SENDMAIL trim($msg) . "\n"; - close SENDMAIL; - } - push(@sentlist, $person); - return 1; -} - -# Code starts here - -ConnectToDatabase(); -GetVersionTable(); - -if (open(FID, ") { - $nomail{trim($_)} = 1; - } - close FID; -} - -# Since any email recipients must be rederived if the user has not -# been rederived since the most recent group change, figure out when that -# is once and determine the need to rederive users using the same DB access -# that gets the user's email address each time a person is processed. -# -SendSQL("SELECT MAX(last_changed) FROM groups"); -($::last_changed) = FetchSQLData(); - -if ($#ARGV >= 0 && $ARGV[0] eq "regenerate") { - print "Regenerating is no longer required or supported\n"; - exit; -} - -if ($#ARGV >= 0 && $ARGV[0] eq "-forcecc") { - shift(@ARGV); - foreach my $i (split(/,/, shift(@ARGV))) { - push(@{$force{'CClist'}}, trim($i)); - } -} - -if ($#ARGV >= 0 && $ARGV[0] eq "-forceowner") { - shift(@ARGV); - @{$force{'Owner'}} = (trim(shift(@ARGV))); -} - -if ($#ARGV >= 0 && $ARGV[0] eq "-forceqacontact") { - shift(@ARGV); - @{$force{'QAcontact'}} = (trim(shift(@ARGV))); -} - -if ($#ARGV >= 0 && $ARGV[0] eq "-forcereporter") { - shift(@ARGV); - @{$force{'Reporter'}} = trim(shift(@ARGV)); -} - -if (($#ARGV < 0) || ($#ARGV > 1)) { - print "Usage:\n processmail {bugid} {nametoexclude} " . - "[-forcecc list,of,users]\n [-forceowner name] " . - "[-forceqacontact name]\n [-forcereporter name]\nor\n" . - " processmail rescanall\n"; - exit; -} - -if ($#ARGV == 1) { - $nametoexclude = lc($ARGV[1]); -} - -if ($ARGV[0] eq "rescanall") { - print "Collecting bug ids...\n"; - SendSQL("select bug_id, lastdiffed, delta_ts from bugs where lastdiffed < delta_ts AND delta_ts < date_sub(now(), INTERVAL 30 minute) order by bug_id"); - my @list; - while (my @row = FetchSQLData()) { - my $time = $row[2]; - if ($time =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/) { - $time = "$1-$2-$3 $4:$5:$6"; - } - print STDERR "Bug $row[0] has unsent mail. lastdiffed is $row[1], delta_ts is $time.\n"; - push @list, $row[0]; - } - if (scalar(@list) > 0) { - print STDERR scalar(@list) . " bugs found with possibly unsent mail\n"; - print STDERR "Updating bugs, sending mail if required\n"; - } else { - print "All appropriate mail appears to have been sent\n" - } - foreach my $id (@list) { - if (detaint_natural($id)) { - ProcessOneBug($id); - } - } -} else { - my $bugnum; - if ($ARGV[0] =~ m/^([1-9][0-9]*)$/) { - $bugnum = $1; - } else { - print "Error calling processmail (bug id is not an integer)
\n"; - exit; - } - ProcessOneBug($bugnum); -} - -exit; diff --git a/sanitycheck.cgi b/sanitycheck.cgi index 49c007f7f..f5b8be210 100755 --- a/sanitycheck.cgi +++ b/sanitycheck.cgi @@ -150,6 +150,31 @@ if (exists $::FORM{'cleangroupsnow'}) { "- reduced from $before records to $after records"); } +if (exists $::FORM{'rescanallBugMail'}) { + require Bugzilla::BugMail; + + Status("OK, now attempting to send unsent mail"); + SendSQL("SELECT bug_id FROM bugs WHERE lastdiffed < delta_ts AND + delta_ts < date_sub(now(), INTERVAL 30 minute) ORDER BY bug_id"); + my @list; + while (MoreSQLData()) { + push (@list, FetchOneColumn()); + } + + Status(scalar(@list) . ' bugs found with possibly unsent mail.'); + + foreach my $bugid (@list) { + Bugzilla::BugMail::Send($bugid); + } + + if (scalar(@list) > 0) { + Status("Unsent mail has been sent."); + } + + PutFooter(); + exit; +} + print "OK, now running sanity checks.

\n"; ########################################################################### @@ -683,7 +708,7 @@ while (@row = FetchSQLData()) { if (@badbugs > 0) { Alert("Bugs that have changes but no mail sent for at least half an hour: " . join (", ", @badbugs)); - print("Run processmail rescanall to fix this

\n"); + print qq{Send these mails.

\n}; } ########################################################################### diff --git a/template/en/default/attachment/created.html.tmpl b/template/en/default/attachment/created.html.tmpl index 56e71ccba..2b985bd0c 100644 --- a/template/en/default/attachment/created.html.tmpl +++ b/template/en/default/attachment/created.html.tmpl @@ -26,7 +26,6 @@ # contenttype: string. The Content Type we attached it as. # contenttypemethod: string. How we got the content type of the attachment. # Possible values: autodetect, list, manual. - # mailresults: string. who was mailed, and who wasn't. #%] [% PROCESS global/header.html.tmpl @@ -42,7 +41,7 @@ to Bug #[% bugid %] Created - [% mailresults %] + [% PROCESS "bug/process/bugmail.html.tmpl" mailing_bugid = bugid %] [% IF contenttypemethod == 'autodetect' %]

diff --git a/template/en/default/attachment/updated.html.tmpl b/template/en/default/attachment/updated.html.tmpl index 2c7ceb24d..df5f4665a 100644 --- a/template/en/default/attachment/updated.html.tmpl +++ b/template/en/default/attachment/updated.html.tmpl @@ -23,7 +23,6 @@ [%# INTERFACE: # bugid: integer. ID of the bug we are updating. # attachid: integer. ID of the attachment we just attached. - # mailresults: string. Who was mailed and who wasn't. #%] [% PROCESS global/header.html.tmpl @@ -40,7 +39,7 @@ attachment [% attachid %] of bug [% bugid %] submitted - [% mailresults %] + [% PROCESS "bug/process/bugmail.html.tmpl" mailing_bugid = bugid %] diff --git a/template/en/default/bug/process/bugmail.html.tmpl b/template/en/default/bug/process/bugmail.html.tmpl new file mode 100644 index 000000000..5445ef9cd --- /dev/null +++ b/template/en/default/bug/process/bugmail.html.tmpl @@ -0,0 +1,70 @@ + +[%# 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): Bradley Baetz + # J. Paul Reed + #%] + +[%# INTERFACE: + # mailing_bugid: string. ID of the bug this mail is concerning. + # mailrecipients: hash. People involved in this change. Hash has up to five + # elements: + # changer: string. The login name of the user who made the + # change. + # + # For bug changes where people need to be notified: + # owner: string. The login name of the bug assignee. + # reporter: string. The login name of the bug reporter. + # qacontact: string. The login name of the bug's QA contact. + # Optional. + # cc: list of strings. The login names of those on the CC + # list. + #%] + +[% mail = SendBugMail(mailing_bugid, mailrecipients) %] + +[% PROCESS emails + description = "Email sent to" + names = mail.sent +%] +
+[% PROCESS emails + description = "Excluding" + names = mail.excluded +%] +
+

+ If you wish to tweak the kinds of mail Bugzilla sends you, you can + change your preferences. +
+ + +[%############################################################################%] +[%# Block for a set of email addresses #%] +[%############################################################################%] + +[% BLOCK emails %] + [% description %]: + [% IF names.size > 0 %] + [%+ FOREACH name = names %] + [% name %][% ", " UNLESS loop.last() %] + [% END %] + [% ELSE %] + no one + [% END %] +[% END %] diff --git a/template/en/default/bug/process/results.html.tmpl b/template/en/default/bug/process/results.html.tmpl index 99dfbc11d..cc3e0df95 100644 --- a/template/en/default/bug/process/results.html.tmpl +++ b/template/en/default/bug/process/results.html.tmpl @@ -25,6 +25,8 @@ # type: string; the type of change/check that was made: "bug" when a bug # is changed, "dupe" when a duplication notation is added to a bug, # and "dep" when a bug is checked for changes to its dependencies. + # + # mailrecipients: hash; BugMail recipient params. Optional. #%] [% DEFAULT type="bug" %] @@ -43,7 +45,7 @@

[% title.$type %]

- [% mail %] + [% PROCESS "bug/process/bugmail.html.tmpl" mailing_bugid = id %] Back To BUG# [% id %] -- cgit v1.2.3-24-g4f1b