BugMail.pm 16.6 KB
Newer Older
1
# -*- Mode: perl; indent-tabs-mode: nil -*-
terry%netscape.com's avatar
terry%netscape.com committed
2
#
3 4 5 6 7 8 9 10 11 12
# 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.
#
terry%netscape.com's avatar
terry%netscape.com committed
13
# The Original Code is the Bugzilla Bug Tracking System.
14
#
terry%netscape.com's avatar
terry%netscape.com committed
15
# The Initial Developer of the Original Code is Netscape Communications
16 17 18 19
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
20
# Contributor(s): Terry Weissman <terry@mozilla.org>,
21 22
#                 Bryce Nesbitt <bryce-mozilla@nextbus.com>
#                 Dan Mosedale <dmose@mozilla.org>
23
#                 Alan Raetz <al_raetz@yahoo.com>
24
#                 Jacob Steenhagen <jake@actex.net>
25
#                 Matthew Tuck <matty@chariot.net.au>
26 27
#                 Bradley Baetz <bbaetz@student.usyd.edu.au>
#                 J. Paul Reed <preed@sigkill.com>
28
#                 Gervase Markham <gerv@gerv.net>
29
#                 Byron Jones <bugzilla@glob.com.au>
30
#                 Reed Loden <reed@reedloden.com>
31
#                 Frédéric Buclin <LpSolit@gmail.com>
32
#                 Guy Pyrzak <guy.pyrzak@gmail.com>
33

34
use strict;
terry%netscape.com's avatar
terry%netscape.com committed
35

36
package Bugzilla::BugMail;
terry%netscape.com's avatar
terry%netscape.com committed
37

38
use Bugzilla::Error;
39
use Bugzilla::User;
40
use Bugzilla::Constants;
41
use Bugzilla::Util;
42
use Bugzilla::Bug;
43
use Bugzilla::Comment;
44
use Bugzilla::Mailer;
45
use Bugzilla::Hook;
46

47 48
use Date::Parse;
use Date::Format;
49 50
use Scalar::Util qw(blessed);
use List::MoreUtils qw(uniq);
51

52 53 54
use constant BIT_DIRECT    => 1;
use constant BIT_WATCHING  => 2;

55 56 57 58 59 60 61 62 63
sub relationships {
    my $ref = RELATIONSHIPS;
    # Clone it so that we don't modify the constant;
    my %relationships = %$ref;
    Bugzilla::Hook::process('bugmail_relationships', 
                            { relationships => \%relationships });
    return %relationships;
}

64 65 66 67 68 69 70 71 72
# 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
73
sub Send {
74 75
    my ($id, $forced, $params) = @_;
    $params ||= {};
76

77
    my $dbh = Bugzilla->dbh;
78
    my $bug = new Bugzilla::Bug($id);
79

80 81
    my $start = $bug->lastdiffed;
    my $end   = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
82

83 84 85 86
    # Bugzilla::User objects of people in various roles. More than one person
    # can 'have' a role, if the person in that role has changed, or people are
    # watching.
    my @assignees = ($bug->assigned_to);
87
    my @qa_contacts = $bug->qa_contact || ();
88

89
    my @ccs = @{ $bug->cc_users };
90 91 92 93 94
    # Include the people passed in as being in particular roles.
    # This can include people who used to hold those roles.
    # At this point, we don't care if there are duplicates in these arrays.
    my $changer = $forced->{'changer'};
    if ($forced->{'owner'}) {
95
        push (@assignees, Bugzilla::User->check($forced->{'owner'}));
96
    }
97
    
98
    if ($forced->{'qacontact'}) {
99
        push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'}));
100
    }
101 102 103
    
    if ($forced->{'cc'}) {
        foreach my $cc (@{$forced->{'cc'}}) {
104
            push(@ccs, Bugzilla::User->check($cc));
105 106
        }
    }
107
    my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs);
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

    my @diffs;
    if (!$start) {
        @diffs = _get_new_bugmail_fields($bug);
    }

    if ($params->{dep_only}) {
        push(@diffs, { field_name => 'bug_status',
                       old => $params->{changes}->{bug_status}->[0],
                       new => $params->{changes}->{bug_status}->[1],
                       login_name => $changer->login,
                       blocker => $params->{blocker} },
                     { field_name => 'resolution',
                       old => $params->{changes}->{resolution}->[0],
                       new => $params->{changes}->{resolution}->[1],
                       login_name => $changer->login,
                       blocker => $params->{blocker} });
    }
    else {
127
        push(@diffs, _get_diffs($bug, $end, \%user_cache));
128 129
    }

130
    my $comments = $bug->comments({ after => $start, to => $end });
131 132
    # Skip empty comments.
    @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
133

134
    ###########################################################################
135
    # Start of email filtering code
136
    ###########################################################################
137
    
138 139
    # A user_id => roles hash to keep track of people.
    my %recipients;
140
    my %watching;
141 142 143 144 145 146
    
    # Now we work out all the people involved with this bug, and note all of
    # the relationships in a hash. The keys are userids, the values are an
    # array of role constants.
    
    # CCs
147
    $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
148 149
    
    # Reporter (there's only ever one)
150
    $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT;
151 152
    
    # QA Contact
153
    if (Bugzilla->params->{'useqacontact'}) {
154 155
        foreach (@qa_contacts) {
            # QA Contact can be blank; ignore it if so.
156
            $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_;
157
        }
158
    }
159

160
    # Assignee
161
    $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees);
162

163 164
    # The last relevant set of people are those who are being removed from 
    # their roles in this change. We get their names out of the diffs.
165 166
    foreach my $change (@diffs) {
        if ($change->{old}) {
167 168
            # You can't stop being the reporter, so we don't check that
            # relationship here.
169
            # Ignore people whose user account has been deleted or renamed.
170 171
            if ($change->{field_name} eq 'cc') {
                foreach my $cc_user (split(/[\s,]+/, $change->{old})) {
172
                    my $uid = login_to_id($cc_user);
173
                    $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid;
174
                }
175
            }
176 177
            elsif ($change->{field_name} eq 'qa_contact') {
                my $uid = login_to_id($change->{old});
178
                $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid;
179
            }
180 181
            elsif ($change->{field_name} eq 'assigned_to') {
                my $uid = login_to_id($change->{old});
182
                $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid;
183
            }
184
        }
185
    }
186 187

    Bugzilla::Hook::process('bugmail_recipients',
188
                            { bug => $bug, recipients => \%recipients });
189

190 191 192 193 194 195 196 197 198 199 200 201 202
    # Find all those user-watching anyone on the current list, who is not
    # on it already themselves.
    my $involved = join(",", keys %recipients);

    my $userwatchers =
        $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
                                  WHERE watched IN ($involved)");

    # Mark these people as having the role of the person they are watching
    foreach my $watch (@$userwatchers) {
        while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
            $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
                if $bits & BIT_DIRECT;
203
        }
204
        push(@{$watching{$watch->[0]}}, $watch->[1]);
205
    }
206 207 208 209 210 211 212 213 214

    # Global watcher
    my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'});
    foreach (@watchers) {
        my $watcher_id = login_to_id($_);
        next unless $watcher_id;
        $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT;
    }

215 216 217 218 219
    # We now have a complete set of all the users, and their relationships to
    # the bug in question. However, we are not necessarily going to mail them
    # all - there are preferences, permissions checks and all sorts to do yet.
    my @sent;
    my @excluded;
220

221
    foreach my $user_id (keys %recipients) {
222
        my %rels_which_want;
223
        my $sent_mail = 0;
224 225
        $user_cache{$user_id} ||= new Bugzilla::User($user_id);
        my $user = $user_cache{$user_id};
226 227 228
        # Deleted users must be excluded.
        next unless $user;

229
        if ($user->can_see_bug($id)) {
230 231
            # Go through each role the user has and see if they want mail in
            # that role.
232
            foreach my $relationship (keys %{$recipients{$user_id}}) {
233
                if ($user->wants_bug_mail($bug,
234
                                          $relationship, 
235
                                          $start ? \@diffs : [],
236
                                          $comments,
237
                                          $params->{dep_only},
238
                                          $changer))
239
                {
240 241
                    $rels_which_want{$relationship} = 
                        $recipients{$user_id}->{$relationship};
242 243
                }
            }
244
        }
245
        
246
        if (scalar(%rels_which_want)) {
247 248 249
            # So the user exists, can see the bug, and wants mail in at least
            # one role. But do we want to send it to them?

250 251 252
            # We shouldn't send mail if this is a dependency mail and the
            # depending bug is not visible to the user.
            # This is to avoid leaking the summary of a confidential bug.
253
            my $dep_ok = 1;
254 255
            if ($params->{dep_only}) {
                $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0;
256 257
            }

258
            # Make sure the user isn't in the nomail list, and the dep check passed.
259
            if ($user->email_enabled && $dep_ok) {
260
                # OK, OK, if we must. Email the user.
261 262 263 264
                $sent_mail = sendMail(
                    { to       => $user, 
                      bug      => $bug,
                      comments => $comments,
265
                      delta_ts => $params->{dep_only} ? $end : undef,
266 267 268
                      changer  => $changer,
                      watchers => exists $watching{$user_id} ?
                                  $watching{$user_id} : undef,
269
                      diffs    => \@diffs,
270 271
                      rels_which_want => \%rels_which_want,
                    });
272
            }
273
        }
274

275 276 277 278 279 280 281
        if ($sent_mail) {
            push(@sent, $user->login); 
        } 
        else {
            push(@excluded, $user->login); 
        } 
    }
282 283 284 285 286 287 288 289 290

    # When sending bugmail about a blocker being reopened or resolved,
    # we say nothing about changes in the bug being blocked, so we must
    # not update lastdiffed in this case.
    if (!$params->{dep_only}) {
        $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?',
                 undef, ($end, $id));
        $bug->{lastdiffed} = $end;
    }
291

292
    return {'sent' => \@sent, 'excluded' => \@excluded};
293 294
}

295
sub sendMail {
296
    my $params = shift;
297
    
298 299 300
    my $user   = $params->{to};
    my $bug    = $params->{bug};
    my @send_comments = @{ $params->{comments} };
301
    my $delta_ts = $params->{delta_ts};
302 303
    my $changer = $params->{changer};
    my $watchingRef = $params->{watchers};
304
    my @diffs = @{ $params->{diffs} };
305 306
    my $relRef      = $params->{rels_which_want};

307 308
    # Only display changes the user is allowed see.
    my @display_diffs;
309

310 311
    foreach my $diff (@diffs) {
        my $add_diff = 0;
312
        
313
        if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) {
314
            $add_diff = 1 if $user->is_timetracker;
315
        }
316 317
        elsif (!$diff->{isprivate} || $user->is_insider) {
            $add_diff = 1;
318
        }
319
        push(@display_diffs, $diff) if $add_diff;
320
    }
321

322
    if (!$user->is_insider) {
323
        @send_comments = grep { !$_->is_private } @send_comments;
324 325
    }

326
    if (!scalar(@display_diffs) && !scalar(@send_comments)) {
327
      # Whoops, no differences!
328
      return 0;
329
    }
330

331 332 333 334
    my (@reasons, @reasons_watch);
    while (my ($relationship, $bits) = each %{$relRef}) {
        push(@reasons, $relationship) if ($bits & BIT_DIRECT);
        push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING);
335
    }
336

337 338 339
    my %relationships = relationships();
    my @headerrel   = map { $relationships{$_} } @reasons;
    my @watchingrel = map { $relationships{$_} } @reasons_watch;
340 341
    push(@headerrel,   'None') unless @headerrel;
    push(@watchingrel, 'None') unless @watchingrel;
342
    push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
343

344
    my $vars = {
345
        delta_ts => $delta_ts,
346
        to_user => $user,
347
        bug => $bug,
348 349
        reasons => \@reasons,
        reasons_watch => \@reasons_watch,
350 351
        reasonsheader => join(" ", @headerrel),
        reasonswatchheader => join(" ", @watchingrel),
352
        changer => $changer,
353 354
        diffs => \@display_diffs,
        changedfields => [uniq map { $_->{field_name} } @display_diffs],
355
        new_comments => \@send_comments,
356
        threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
357
    };
358
    my $msg =  _generate_bugmail($user, $vars);
359 360 361 362 363
    MessageToMTA($msg);

    return 1;
}

364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
sub _generate_bugmail {
    my ($user, $vars) = @_;
    my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
    my ($msg_text, $msg_html, $msg_header);
  
    $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header)
        || ThrowTemplateError($template->error());
    $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text)
        || ThrowTemplateError($template->error());
    $template->process("email/bugmail.html.tmpl", $vars, \$msg_html)
        || ThrowTemplateError($template->error());
    
    my @parts = (
        Email::MIME->create(
            attributes => {
                content_type => "text/plain",
            },
            body => $msg_text,
        ),
        Email::MIME->create(
            attributes => {
                content_type => "text/html",         
            },
            body => $msg_html,
        ),
    );
  
    my $email = new Email::MIME($msg_header);
    $email->parts_set(\@parts);
    $email->content_type_set('multipart/alternative');
    return $email;
}

397
sub _get_diffs {
398
    my ($bug, $end, $user_cache) = @_;
399 400 401 402 403 404 405 406 407 408 409
    my $dbh = Bugzilla->dbh;

    my @args = ($bug->id);
    # If lastdiffed is NULL, then we don't limit the search on time.
    my $when_restriction = '';
    if ($bug->lastdiffed) {
        $when_restriction = ' AND bug_when > ? AND bug_when <= ?';
        push @args, ($bug->lastdiffed, $end);
    }

    my $diffs = $dbh->selectall_arrayref(
410
           "SELECT fielddefs.name AS field_name,
411 412
                   bugs_activity.bug_when, bugs_activity.removed AS old,
                   bugs_activity.added AS new, bugs_activity.attach_id,
413
                   bugs_activity.comment_id, bugs_activity.who
414 415 416 417 418
              FROM bugs_activity
        INNER JOIN fielddefs
                ON fielddefs.id = bugs_activity.fieldid
             WHERE bugs_activity.bug_id = ?
                   $when_restriction
419 420 421
          ORDER BY bugs_activity.bug_when", {Slice=>{}}, @args);

    foreach my $diff (@$diffs) {
422 423
        $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); 
        $diff->{who} =  $user_cache->{$diff->{who}};
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
        if ($diff->{attach_id}) {
            $diff->{isprivate} = $dbh->selectrow_array(
                'SELECT isprivate FROM attachments WHERE attach_id = ?',
                undef, $diff->{attach_id});
         }
         if ($diff->{field_name} eq 'longdescs.isprivate') {
             my $comment = Bugzilla::Comment->new($diff->{comment_id});
             $diff->{num} = $comment->count;
             $diff->{isprivate} = $diff->{new};
         }
    }

    return @$diffs;
}

sub _get_new_bugmail_fields {
    my $bug = shift;
441
    my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) };
442 443 444 445 446 447 448 449
    my @diffs;

    foreach my $field (@fields) {
        my $name = $field->name;
        my $value = $bug->$name;

        if (ref $value eq 'ARRAY') {
            $value = join(', ', @$value);
450
        }
451 452
        elsif (blessed($value) && $value->isa('Bugzilla::User')) {
            $value = $value->login;
453
        }
454 455 456 457 458 459 460
        elsif (blessed($value) && $value->isa('Bugzilla::Object')) {
            $value = $value->name;
        }
        elsif ($name eq 'estimated_time') {
            # "0.00" (which is what we get from the DB) is true,
            # so we explicitly do a numerical comparison with 0.
            $value = 0 if $value == 0;
461
        }
462 463
        elsif ($name eq 'deadline') {
            $value = time2str("%Y-%m-%d", str2time($value)) if $value;
464
        }
465 466 467 468 469

        # If there isn't anything to show, don't include this header.
        next unless $value;

        push(@diffs, {field_name => $name, new => $value});
470 471
    }

472
    return @diffs;
473 474
}

475
1;