r85352 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r85351‎ | r85352 | r85353 >
Date:18:13, 4 April 2011
Author:pdhanda
Status:deferred
Tags:
Comment:
Copied from the Bugzilla4.0 release without modifications.
Modified paths:
  • /trunk/tools/bugzilla/bugzilla-4.0/Bugzilla (added) (history)
  • /trunk/tools/bugzilla/bugzilla-4.0/Bugzilla/BugMail.pm (added) (history)

Diff [purge]

Index: trunk/tools/bugzilla/bugzilla-4.0/Bugzilla/BugMail.pm
@@ -0,0 +1,587 @@
 2+# -*- Mode: perl; indent-tabs-mode: nil -*-
 3+#
 4+# The contents of this file are subject to the Mozilla Public
 5+# License Version 1.1 (the "License"); you may not use this file
 6+# except in compliance with the License. You may obtain a copy of
 7+# the License at http://www.mozilla.org/MPL/
 8+#
 9+# Software distributed under the License is distributed on an "AS
 10+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 11+# implied. See the License for the specific language governing
 12+# rights and limitations under the License.
 13+#
 14+# The Original Code is the Bugzilla Bug Tracking System.
 15+#
 16+# The Initial Developer of the Original Code is Netscape Communications
 17+# Corporation. Portions created by Netscape are
 18+# Copyright (C) 1998 Netscape Communications Corporation. All
 19+# Rights Reserved.
 20+#
 21+# Contributor(s): Terry Weissman <terry@mozilla.org>,
 22+# Bryce Nesbitt <bryce-mozilla@nextbus.com>
 23+# Dan Mosedale <dmose@mozilla.org>
 24+# Alan Raetz <al_raetz@yahoo.com>
 25+# Jacob Steenhagen <jake@actex.net>
 26+# Matthew Tuck <matty@chariot.net.au>
 27+# Bradley Baetz <bbaetz@student.usyd.edu.au>
 28+# J. Paul Reed <preed@sigkill.com>
 29+# Gervase Markham <gerv@gerv.net>
 30+# Byron Jones <bugzilla@glob.com.au>
 31+# Reed Loden <reed@reedloden.com>
 32+
 33+use strict;
 34+
 35+package Bugzilla::BugMail;
 36+
 37+use Bugzilla::Error;
 38+use Bugzilla::User;
 39+use Bugzilla::Constants;
 40+use Bugzilla::Util;
 41+use Bugzilla::Bug;
 42+use Bugzilla::Classification;
 43+use Bugzilla::Product;
 44+use Bugzilla::Component;
 45+use Bugzilla::Status;
 46+use Bugzilla::Mailer;
 47+use Bugzilla::Hook;
 48+
 49+use Date::Parse;
 50+use Date::Format;
 51+
 52+use constant FORMAT_TRIPLE => "%19s|%-28s|%-28s";
 53+use constant FORMAT_3_SIZE => [19,28,28];
 54+use constant FORMAT_DOUBLE => "%19s %-55s";
 55+use constant FORMAT_2_SIZE => [19,55];
 56+
 57+use constant BIT_DIRECT => 1;
 58+use constant BIT_WATCHING => 2;
 59+
 60+# We use this instead of format because format doesn't deal well with
 61+# multi-byte languages.
 62+sub multiline_sprintf {
 63+ my ($format, $args, $sizes) = @_;
 64+ my @parts;
 65+ my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
 66+ foreach my $string (@$args) {
 67+ my $size = shift @my_sizes;
 68+ my @pieces = split("\n", wrap_hard($string, $size));
 69+ push(@parts, \@pieces);
 70+ }
 71+
 72+ my $formatted;
 73+ while (1) {
 74+ # Get the first item of each part.
 75+ my @line = map { shift @$_ } @parts;
 76+ # If they're all undef, we're done.
 77+ last if !grep { defined $_ } @line;
 78+ # Make any single undef item into ''
 79+ @line = map { defined $_ ? $_ : '' } @line;
 80+ # And append a formatted line
 81+ $formatted .= sprintf($format, @line);
 82+ # Remove trailing spaces, or they become lots of =20's in
 83+ # quoted-printable emails.
 84+ $formatted =~ s/\s+$//;
 85+ $formatted .= "\n";
 86+ }
 87+ return $formatted;
 88+}
 89+
 90+sub three_columns {
 91+ return multiline_sprintf(FORMAT_TRIPLE, \@_, FORMAT_3_SIZE);
 92+}
 93+
 94+sub relationships {
 95+ my $ref = RELATIONSHIPS;
 96+ # Clone it so that we don't modify the constant;
 97+ my %relationships = %$ref;
 98+ Bugzilla::Hook::process('bugmail_relationships',
 99+ { relationships => \%relationships });
 100+ return %relationships;
 101+}
 102+
 103+# This is a bit of a hack, basically keeping the old system()
 104+# cmd line interface. Should clean this up at some point.
 105+#
 106+# args: bug_id, and an optional hash ref which may have keys for:
 107+# changer, owner, qa, reporter, cc
 108+# Optional hash contains values of people which will be forced to those
 109+# roles when the email is sent.
 110+# All the names are email addresses, not userids
 111+# values are scalars, except for cc, which is a list
 112+sub Send {
 113+ my ($id, $forced) = (@_);
 114+
 115+ my $dbh = Bugzilla->dbh;
 116+ my $bug = new Bugzilla::Bug($id);
 117+
 118+ # Only used for headers in bugmail for new bugs
 119+ my @fields = Bugzilla->get_fields({obsolete => 0, mailhead => 1});
 120+
 121+ my $start = $bug->lastdiffed;
 122+ my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
 123+
 124+ # Bugzilla::User objects of people in various roles. More than one person
 125+ # can 'have' a role, if the person in that role has changed, or people are
 126+ # watching.
 127+ my @assignees = ($bug->assigned_to);
 128+ my @qa_contacts = ($bug->qa_contact);
 129+
 130+ my @ccs = @{ $bug->cc_users };
 131+
 132+ # Include the people passed in as being in particular roles.
 133+ # This can include people who used to hold those roles.
 134+ # At this point, we don't care if there are duplicates in these arrays.
 135+ my $changer = $forced->{'changer'};
 136+ if ($forced->{'owner'}) {
 137+ push (@assignees, Bugzilla::User->check($forced->{'owner'}));
 138+ }
 139+
 140+ if ($forced->{'qacontact'}) {
 141+ push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'}));
 142+ }
 143+
 144+ if ($forced->{'cc'}) {
 145+ foreach my $cc (@{$forced->{'cc'}}) {
 146+ push(@ccs, Bugzilla::User->check($cc));
 147+ }
 148+ }
 149+
 150+ my @args = ($bug->id);
 151+
 152+ # If lastdiffed is NULL, then we don't limit the search on time.
 153+ my $when_restriction = '';
 154+ if ($start) {
 155+ $when_restriction = ' AND bug_when > ? AND bug_when <= ?';
 156+ push @args, ($start, $end);
 157+ }
 158+
 159+ my $diffs = $dbh->selectall_arrayref(
 160+ "SELECT profiles.login_name, profiles.realname, fielddefs.description,
 161+ bugs_activity.bug_when, bugs_activity.removed,
 162+ bugs_activity.added, bugs_activity.attach_id, fielddefs.name,
 163+ bugs_activity.comment_id
 164+ FROM bugs_activity
 165+ INNER JOIN fielddefs
 166+ ON fielddefs.id = bugs_activity.fieldid
 167+ INNER JOIN profiles
 168+ ON profiles.userid = bugs_activity.who
 169+ WHERE bugs_activity.bug_id = ?
 170+ $when_restriction
 171+ ORDER BY bugs_activity.bug_when", undef, @args);
 172+
 173+ my @new_depbugs;
 174+ my $difftext = "";
 175+ my $diffheader = "";
 176+ my @diffparts;
 177+ my $lastwho = "";
 178+ my $fullwho;
 179+ my @changedfields;
 180+ foreach my $ref (@$diffs) {
 181+ my ($who, $whoname, $what, $when, $old, $new, $attachid, $fieldname, $comment_id) = (@$ref);
 182+ my $diffpart = {};
 183+ if ($who ne $lastwho) {
 184+ $lastwho = $who;
 185+ $fullwho = $whoname ? "$whoname <$who>" : $who;
 186+ $diffheader = "\n$fullwho changed:\n\n";
 187+ $diffheader .= three_columns("What ", "Removed", "Added");
 188+ $diffheader .= ('-' x 76) . "\n";
 189+ }
 190+ $what =~ s/^(Attachment )?/Attachment #$attachid / if $attachid;
 191+ if( $fieldname eq 'estimated_time' ||
 192+ $fieldname eq 'remaining_time' ) {
 193+ $old = format_time_decimal($old);
 194+ $new = format_time_decimal($new);
 195+ }
 196+ if ($fieldname eq 'dependson') {
 197+ push(@new_depbugs, grep {$_ =~ /^\d+$/} split(/[\s,]+/, $new));
 198+ }
 199+ if ($attachid) {
 200+ ($diffpart->{'isprivate'}) = $dbh->selectrow_array(
 201+ 'SELECT isprivate FROM attachments WHERE attach_id = ?',
 202+ undef, ($attachid));
 203+ }
 204+ if ($fieldname eq 'longdescs.isprivate') {
 205+ my $comment = Bugzilla::Comment->new($comment_id);
 206+ my $comment_num = $comment->count;
 207+ $what =~ s/^(Comment )?/Comment #$comment_num /;
 208+ $diffpart->{'isprivate'} = $new;
 209+ }
 210+ $difftext = three_columns($what, $old, $new);
 211+ $diffpart->{'header'} = $diffheader;
 212+ $diffpart->{'fieldname'} = $fieldname;
 213+ $diffpart->{'text'} = $difftext;
 214+ push(@diffparts, $diffpart);
 215+ push(@changedfields, $what);
 216+ }
 217+
 218+ my @depbugs;
 219+ my $deptext = "";
 220+ # Do not include data about dependent bugs when they have just been added.
 221+ # Completely skip checking for dependent bugs on bug creation as all
 222+ # dependencies bugs will just have been added.
 223+ if ($start) {
 224+ my $dep_restriction = "";
 225+ if (scalar @new_depbugs) {
 226+ $dep_restriction = "AND bugs_activity.bug_id NOT IN (" .
 227+ join(", ", @new_depbugs) . ")";
 228+ }
 229+
 230+ my $dependency_diffs = $dbh->selectall_arrayref(
 231+ "SELECT bugs_activity.bug_id, bugs.short_desc, fielddefs.name,
 232+ fielddefs.description, bugs_activity.removed,
 233+ bugs_activity.added
 234+ FROM bugs_activity
 235+ INNER JOIN bugs
 236+ ON bugs.bug_id = bugs_activity.bug_id
 237+ INNER JOIN dependencies
 238+ ON bugs_activity.bug_id = dependencies.dependson
 239+ INNER JOIN fielddefs
 240+ ON fielddefs.id = bugs_activity.fieldid
 241+ WHERE dependencies.blocked = ?
 242+ AND (fielddefs.name = 'bug_status'
 243+ OR fielddefs.name = 'resolution')
 244+ $when_restriction
 245+ $dep_restriction
 246+ ORDER BY bugs_activity.bug_when, bugs.bug_id", undef, @args);
 247+
 248+ my $thisdiff = "";
 249+ my $lastbug = "";
 250+ my $interestingchange = 0;
 251+ foreach my $dependency_diff (@$dependency_diffs) {
 252+ my ($depbug, $summary, $fieldname, $what, $old, $new) = @$dependency_diff;
 253+
 254+ if ($depbug ne $lastbug) {
 255+ if ($interestingchange) {
 256+ $deptext .= $thisdiff;
 257+ }
 258+ $lastbug = $depbug;
 259+ $thisdiff =
 260+ "\nBug $id depends on bug $depbug, which changed state.\n\n" .
 261+ "Bug $depbug Summary: $summary\n" .
 262+ correct_urlbase() . "show_bug.cgi?id=$depbug\n\n";
 263+ $thisdiff .= three_columns("What ", "Old Value", "New Value");
 264+ $thisdiff .= ('-' x 76) . "\n";
 265+ $interestingchange = 0;
 266+ }
 267+ $thisdiff .= three_columns($what, $old, $new);
 268+ if ($fieldname eq 'bug_status'
 269+ && is_open_state($old) ne is_open_state($new))
 270+ {
 271+ $interestingchange = 1;
 272+ }
 273+ push(@depbugs, $depbug);
 274+ }
 275+
 276+ if ($interestingchange) {
 277+ $deptext .= $thisdiff;
 278+ }
 279+ $deptext = trim($deptext);
 280+
 281+ if ($deptext) {
 282+ my $diffpart = {};
 283+ $diffpart->{'text'} = "\n" . trim($deptext);
 284+ push(@diffparts, $diffpart);
 285+ }
 286+ }
 287+
 288+ my $comments = $bug->comments({ after => $start, to => $end });
 289+ # Skip empty comments.
 290+ @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
 291+
 292+ ###########################################################################
 293+ # Start of email filtering code
 294+ ###########################################################################
 295+
 296+ # A user_id => roles hash to keep track of people.
 297+ my %recipients;
 298+ my %watching;
 299+
 300+ # Now we work out all the people involved with this bug, and note all of
 301+ # the relationships in a hash. The keys are userids, the values are an
 302+ # array of role constants.
 303+
 304+ # CCs
 305+ $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
 306+
 307+ # Reporter (there's only ever one)
 308+ $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT;
 309+
 310+ # QA Contact
 311+ if (Bugzilla->params->{'useqacontact'}) {
 312+ foreach (@qa_contacts) {
 313+ # QA Contact can be blank; ignore it if so.
 314+ $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_;
 315+ }
 316+ }
 317+
 318+ # Assignee
 319+ $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees);
 320+
 321+ # The last relevant set of people are those who are being removed from
 322+ # their roles in this change. We get their names out of the diffs.
 323+ foreach my $ref (@$diffs) {
 324+ my ($who, $whoname, $what, $when, $old, $new) = (@$ref);
 325+ if ($old) {
 326+ # You can't stop being the reporter, so we don't check that
 327+ # relationship here.
 328+ # Ignore people whose user account has been deleted or renamed.
 329+ if ($what eq "CC") {
 330+ foreach my $cc_user (split(/[\s,]+/, $old)) {
 331+ my $uid = login_to_id($cc_user);
 332+ $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid;
 333+ }
 334+ }
 335+ elsif ($what eq "QAContact") {
 336+ my $uid = login_to_id($old);
 337+ $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid;
 338+ }
 339+ elsif ($what eq "AssignedTo") {
 340+ my $uid = login_to_id($old);
 341+ $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid;
 342+ }
 343+ }
 344+ }
 345+
 346+ Bugzilla::Hook::process('bugmail_recipients',
 347+ { bug => $bug, recipients => \%recipients,
 348+ diffs => $diffs });
 349+
 350+ # Find all those user-watching anyone on the current list, who is not
 351+ # on it already themselves.
 352+ my $involved = join(",", keys %recipients);
 353+
 354+ my $userwatchers =
 355+ $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
 356+ WHERE watched IN ($involved)");
 357+
 358+ # Mark these people as having the role of the person they are watching
 359+ foreach my $watch (@$userwatchers) {
 360+ while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
 361+ $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
 362+ if $bits & BIT_DIRECT;
 363+ }
 364+ push(@{$watching{$watch->[0]}}, $watch->[1]);
 365+ }
 366+
 367+ # Global watcher
 368+ my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'});
 369+ foreach (@watchers) {
 370+ my $watcher_id = login_to_id($_);
 371+ next unless $watcher_id;
 372+ $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT;
 373+ }
 374+
 375+ # We now have a complete set of all the users, and their relationships to
 376+ # the bug in question. However, we are not necessarily going to mail them
 377+ # all - there are preferences, permissions checks and all sorts to do yet.
 378+ my @sent;
 379+ my @excluded;
 380+
 381+ foreach my $user_id (keys %recipients) {
 382+ my %rels_which_want;
 383+ my $sent_mail = 0;
 384+ my $user = new Bugzilla::User($user_id);
 385+ # Deleted users must be excluded.
 386+ next unless $user;
 387+
 388+ if ($user->can_see_bug($id)) {
 389+ # Go through each role the user has and see if they want mail in
 390+ # that role.
 391+ foreach my $relationship (keys %{$recipients{$user_id}}) {
 392+ if ($user->wants_bug_mail($id,
 393+ $relationship,
 394+ $diffs,
 395+ $comments,
 396+ $deptext,
 397+ $changer,
 398+ !$start))
 399+ {
 400+ $rels_which_want{$relationship} =
 401+ $recipients{$user_id}->{$relationship};
 402+ }
 403+ }
 404+ }
 405+
 406+ if (scalar(%rels_which_want)) {
 407+ # So the user exists, can see the bug, and wants mail in at least
 408+ # one role. But do we want to send it to them?
 409+
 410+ # We shouldn't send mail if this is a dependency mail (i.e. there
 411+ # is something in @depbugs), and any of the depending bugs are not
 412+ # visible to the user. This is to avoid leaking the summaries of
 413+ # confidential bugs.
 414+ my $dep_ok = 1;
 415+ foreach my $dep_id (@depbugs) {
 416+ if (!$user->can_see_bug($dep_id)) {
 417+ $dep_ok = 0;
 418+ last;
 419+ }
 420+ }
 421+
 422+ # Make sure the user isn't in the nomail list, and the insider and
 423+ # dep checks passed.
 424+ if ($user->email_enabled && $dep_ok) {
 425+ # OK, OK, if we must. Email the user.
 426+ $sent_mail = sendMail(
 427+ { to => $user,
 428+ fields => \@fields,
 429+ bug => $bug,
 430+ comments => $comments,
 431+ is_new => !$start,
 432+ changer => $changer,
 433+ watchers => exists $watching{$user_id} ?
 434+ $watching{$user_id} : undef,
 435+ diff_parts => \@diffparts,
 436+ rels_which_want => \%rels_which_want,
 437+ changed_fields => \@changedfields,
 438+ });
 439+ }
 440+ }
 441+
 442+ if ($sent_mail) {
 443+ push(@sent, $user->login);
 444+ }
 445+ else {
 446+ push(@excluded, $user->login);
 447+ }
 448+ }
 449+
 450+ $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?',
 451+ undef, ($end, $id));
 452+ $bug->{lastdiffed} = $end;
 453+
 454+ return {'sent' => \@sent, 'excluded' => \@excluded};
 455+}
 456+
 457+sub sendMail {
 458+ my $params = shift;
 459+
 460+ my $user = $params->{to};
 461+ my @fields = @{ $params->{fields} };
 462+ my $bug = $params->{bug};
 463+ my @send_comments = @{ $params->{comments} };
 464+ my $isnew = $params->{is_new};
 465+ my $changer = $params->{changer};
 466+ my $watchingRef = $params->{watchers};
 467+ my @diffparts = @{ $params->{diff_parts} };
 468+ my $relRef = $params->{rels_which_want};
 469+ my @changed_fields = @{ $params->{changed_fields} };
 470+
 471+ # Build difftext (the actions) by verifying the user should see them
 472+ my $difftext = "";
 473+ my $diffheader = "";
 474+ my $add_diff;
 475+
 476+ foreach my $diff (@diffparts) {
 477+ $add_diff = 0;
 478+
 479+ if (exists($diff->{'fieldname'}) &&
 480+ ($diff->{'fieldname'} eq 'estimated_time' ||
 481+ $diff->{'fieldname'} eq 'remaining_time' ||
 482+ $diff->{'fieldname'} eq 'work_time' ||
 483+ $diff->{'fieldname'} eq 'deadline'))
 484+ {
 485+ $add_diff = 1 if $user->is_timetracker;
 486+ } elsif ($diff->{'isprivate'}
 487+ && !$user->is_insider)
 488+ {
 489+ $add_diff = 0;
 490+ } else {
 491+ $add_diff = 1;
 492+ }
 493+
 494+ if ($add_diff) {
 495+ if (exists($diff->{'header'}) &&
 496+ ($diffheader ne $diff->{'header'})) {
 497+ $diffheader = $diff->{'header'};
 498+ $difftext .= $diffheader;
 499+ }
 500+ $difftext .= $diff->{'text'};
 501+ }
 502+ }
 503+
 504+ if (!$user->is_insider) {
 505+ @send_comments = grep { !$_->is_private } @send_comments;
 506+ }
 507+
 508+ if ($difftext eq "" && !scalar(@send_comments) && !$isnew) {
 509+ # Whoops, no differences!
 510+ return 0;
 511+ }
 512+
 513+ my $diffs = $difftext;
 514+ # Remove extra newlines.
 515+ $diffs =~ s/^\n+//s; $diffs =~ s/\n+$//s;
 516+ if ($isnew) {
 517+ my $head = "";
 518+ foreach my $field (@fields) {
 519+ my $name = $field->name;
 520+ my $value = $bug->$name;
 521+
 522+ if (ref $value eq 'ARRAY') {
 523+ $value = join(', ', @$value);
 524+ }
 525+ elsif (ref $value && $value->isa('Bugzilla::User')) {
 526+ $value = $value->login;
 527+ }
 528+ elsif (ref $value && $value->isa('Bugzilla::Object')) {
 529+ $value = $value->name;
 530+ }
 531+ elsif ($name eq 'estimated_time') {
 532+ $value = ($value == 0) ? 0 : format_time_decimal($value);
 533+ }
 534+ elsif ($name eq 'deadline') {
 535+ $value = time2str("%Y-%m-%d", str2time($value)) if $value;
 536+ }
 537+
 538+ # If there isn't anything to show, don't include this header.
 539+ next unless $value;
 540+ # Only send estimated_time if it is enabled and the user is in the group.
 541+ if (($name ne 'estimated_time' && $name ne 'deadline') || $user->is_timetracker) {
 542+ my $desc = $field->description;
 543+ $head .= multiline_sprintf(FORMAT_DOUBLE, ["$desc:", $value],
 544+ FORMAT_2_SIZE);
 545+ }
 546+ }
 547+ $diffs = $head . ($difftext ? "\n\n" : "") . $diffs;
 548+ }
 549+
 550+ my (@reasons, @reasons_watch);
 551+ while (my ($relationship, $bits) = each %{$relRef}) {
 552+ push(@reasons, $relationship) if ($bits & BIT_DIRECT);
 553+ push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING);
 554+ }
 555+
 556+ my %relationships = relationships();
 557+ my @headerrel = map { $relationships{$_} } @reasons;
 558+ my @watchingrel = map { $relationships{$_} } @reasons_watch;
 559+ push(@headerrel, 'None') unless @headerrel;
 560+ push(@watchingrel, 'None') unless @watchingrel;
 561+ push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
 562+
 563+ my $vars = {
 564+ isnew => $isnew,
 565+ to_user => $user,
 566+ bug => $bug,
 567+ changedfields => \@changed_fields,
 568+ reasons => \@reasons,
 569+ reasons_watch => \@reasons_watch,
 570+ reasonsheader => join(" ", @headerrel),
 571+ reasonswatchheader => join(" ", @watchingrel),
 572+ changer => $changer,
 573+ diffs => $diffs,
 574+ new_comments => \@send_comments,
 575+ threadingmarker => build_thread_marker($bug->id, $user->id, $isnew),
 576+ };
 577+
 578+ my $msg;
 579+ my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
 580+ $template->process("email/newchangedmail.txt.tmpl", $vars, \$msg)
 581+ || ThrowTemplateError($template->error());
 582+
 583+ MessageToMTA($msg);
 584+
 585+ return 1;
 586+}
 587+
 588+1;

Status & tagging log