summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/BugMail.pm879
-rw-r--r--Bugzilla/Template.pm7
2 files changed, 886 insertions, 0 deletions
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 <terry@mozilla.org>,
+# Bryce Nesbitt <bryce-mozilla@nextbus.com>
+# Dan Mosedale <dmose@mozilla.org>
+# Alan Raetz <al_raetz@yahoo.com>
+# Jacob Steenhagen <jake@actex.net>
+# Matthew Tuck <matty@chariot.net.au>
+# Bradley Baetz <bbaetz@student.usyd.edu.au>
+# J. Paul Reed <preed@sigkill.com>
+
+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, "<data/nomail")) {
+ while (<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<br>};
+
+ # 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), "<br>\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,