1 # -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*-
3 ################################
5 ################################
8 package BotModules::Bugzilla;
10 @ISA = qw(BotModules);
13 use Fcntl qw(:DEFAULT :flock);
16 # For parsing bugmail.log records. Must be the same as
17 # FIELD_SEPARATOR in bugmail.pl.
18 use constant FIELD_SEPARATOR => '::::';
19 # The log file that we read to report bug changes.
20 # This will be put in the directory returned by dirname($0).
21 use constant BUGMAIL_LOG => 'BotModules/.bugmail.log';
24 # there is a minor error in this module: bugsHistory->$target->$bug is
25 # accessed even when bugsHistory->$target doesn't yet exist. XXX
27 # This is ported straight from techbot, so some of the code is a little convoluted. So sue me. I was lazy.
31 my $retval = $self->SUPER::Initialise(@_);
32 my ($throw_away) = $self->GetBugLog();
33 $throw_away->truncate(0) if $throw_away;
34 $throw_away->close() if $throw_away;
38 # RegisterConfig - Called when initialised, should call registerVariables
41 $self->SUPER::RegisterConfig(@_);
42 $self->registerVariables(
43 # [ name, save?, settable? ]
44 ['bugsURI', 1, 1, 'https://bugzilla.mozilla.org/'],
45 ['bugsDWIMQueryDefault', 1, 1, 'short_desc_type=substring&short_desc='],
46 ['bugsDWIMQueryChannelDefault', 1, 1, {}],
47 ['bugsHistory', 0, 0, {}],
48 ['backoffTime', 1, 1, 120],
49 ['ignoreCommentsTo', 1, 1, ['']],
50 ['ignoreCommentsFrom', 1, 1, ['|']],
51 ['mailIgnore', 1, 1, []],
52 ['skipPrefixFor', 1, 1, []],
53 # The keys for productReportChannels can be in the form of 'Product'
54 # or 'Product::::Component'. The value is a comma-separated list of
56 ['productReportChannels', 1, 1, {}],
57 # The fields that you want notifications about.
58 ['reportFields', 1, 1, ['Resolution', 'Flag', 'Attachment Flag',
59 'NewBug', 'NewAttach']],
60 # Except in these products, you don't want notifications about
61 # certain fields (key is product name, value is comma-separated
63 ['productMuteFields', 1, 1, {}],
64 # And in these channels, you don't want notifications about certain
65 # fields (the key is the channel name and the value is a
66 # comma-separated list of fields).
67 ['channelMuteFields', 1, 1, {}],
68 # How frequently we check for new bugmail we've received, in seconds.
69 ['updateDelay', 1, 1, 10],
70 # List of products for which component of new bugs is reported instead
71 # of only the product. Can also restrict to specific components
72 # by using Product::::Component syntax and always report components
74 ['reportComponent', 1, 1, ['all']],
75 ['mutes', 1, 1, ''], # "channel channel channel"
76 # Optionally skip fetching the bug details for automatic notifications
77 ['reportBugDetails', 1, 1, 1]
85 '' => 'The Bugzilla module provides an interface to the bugzilla bug database. It will spot anyone mentioning bugs, too, and report on what they are. For example if someone says \'I think that\'s a dup of bug 5693, the :hover thing\', then this module will display information about bug 5693.',
86 'bug' => 'Fetches a summary of bugs from bugzilla. Expert syntax: \'bugzilla [bugnumber[,]]*[&bugzillaparameter=value]*\', bug_status: UNCONFIRMED|NEW|ASSIGNED|REOPENED; *type*=substring|; bugtype: include|exclude; order: Assignee|; chfield[from|to|value] short_desc\' long_desc\' status_whiteboard\' bug_file_loc\' keywords\'; \'_type; email[|type][1|2] [reporter|qa_contact|assigned_to|cc]',
87 'bug-total' => 'Same as bug (which see) but only displays the total line.',
88 'bugs' => q{A simple DWIM search. Not very clever. ;-)}
89 . q{ Syntax: '<query string> bugs' e.g. 'mozbot bugs'.},
90 'ignore' => q{Causes the bot to stop reporting all bug changes}
91 . q{ made by a particular user in the current channel.}
92 . q{ Syntax: 'ignore <user@domain.com>' },
93 'unignore' => q{Causes the bot to un-ignore a previously ignored}
94 . q{ user. See 'ignore'}
95 . q{ for more details.},
97 if ($self->isAdmin($event)) {
98 $commands{'mute'} = 'Disable watching for bug numbers in a channel. Syntax: mute bugzilla in <channel>';
99 $commands{'unmute'} = 'Enable watching for bug numbers in a channel. Syntax: unmute bugzilla in <channel>';
104 # Schedule - called when bot connects to a server, to install any schedulers
105 # use $self->schedule($event, $delay, $times, $data)
106 # where $times is 1 for a single event, -1 for recurring events,
107 # and a +ve number for an event that occurs that many times.
111 $self->schedule($event, \$self->{'updateDelay'}, -1, 'Bugzilla-BugMail');
112 return $self->SUPER::Schedule($event);
117 my ($event, @data) = @_;
118 if ($data[0] eq 'Bugzilla-BugMail') {
119 $self->CheckForBugMail($event);
121 return $self->SUPER::Scheduled($event, @data);
128 my ($event, $message) = @_;
129 if ($message =~ /^\s*ignore (.+)[?!.\s]*$/) {
131 # If we aren't already ignoring them...
132 if (!grep($_ eq $user, @{$self->{'mailIgnore'}})) {
133 push (@{$self->{'mailIgnore'}}, $user);
136 "$event->{'from'}: OK, ignoring changes produced by $user.");
140 "$event->{'from'}: $user is already being ignored.");
143 elsif ($message =~ /^\s*unignore (.+)[?!.\s]*$/) {
145 my %ignoredUsers = map { $_ => 1 } @{$self->{'mailIgnore'}};
146 # If we are already ignoring them...
147 if ($ignoredUsers{$user}) {
148 delete $ignoredUsers{$user};
149 $self->{'mailIgnore'} = [keys %ignoredUsers];
152 "$event->{'from'}: OK, $user is no longer being ignored.");
155 $self->say($event, "$event->{'from'}: $user wasn't being ignored.");
158 elsif ($message =~ m/^ \s* # some optional whitespace
159 (?:please\s+)? # an optional "please", followed optionally by either:
160 (?: (?:could\s+you\s+)? # 1. an optional "could you",
161 (?:please\s+)? # another optional "please",
162 show\s+me\s+ | # and the text "show me"
163 what\s+is\s+ | # 2. the text "what is"
164 what\'s\s+ )? # 3. or the text "what's"
165 bug (?:\s*id)?s? [\#\s]+ # a variant on "bug", "bug id", "bugids", etc
166 ([0-9].*?| # a query string, either a number followed by some optional text, or
167 &.+?) # a query string, starting with a &.
168 (?:\s+please)? # followed by yet another optional "please"
169 [?!.\s]* # ending with some optional punctuation
171 my $target = $event->{'target'};
173 # Single bugs use xml.cgi, because then we get error messages
174 if ($bug =~ m/^\d+$/) {
175 $self->FetchBug($event, $bug, 'bug', {'sayAlways' => 1});
177 $self->FetchBug($event, $bug, 'bugs', {'sayAlways' => 1});
179 $self->{'bugsHistory'}->{$target}->{$bug} = $event->{'time'} if $bug =~ m/^\d+$/os;
180 } elsif ($message =~ m/^\s*bug-?total\s+(.+?)\s*$/osi) {
181 $self->FetchBug($event, $1, 'total');
182 } elsif ($self->isAdmin($event)) {
183 if ($message =~ m/^\s*mute\s+bugzilla\s+in\s+(\S+?)\s*$/osi) {
184 $self->{'mutes'} .= " $1";
186 $self->say($event, "$event->{'from'}: Watching for bug numbers disabled in channel $1.");
187 } elsif ($message =~ m/^\s*unmute\s+bugzilla\s+in\s+(\S+)\s*$/osi) {
188 my %mutedChannels = map { $_ => 1 } split(/ /o, $self->{'mutes'});
189 delete($mutedChannels{$1}); # get rid of any mentions of that channel
190 $self->{'mutes'} = join(' ', keys(%mutedChannels));
192 $self->say($event, "$event->{'from'}: Watching for bug numbers reenabled in channel $1.");
194 return $self->SUPER::Told(@_);
197 return $self->SUPER::Told(@_);
199 return 0; # dealt with it...
204 my ($event, $message) = @_;
205 if ((($event->{'channel'} eq '') or # either it was /msg'ed, or
206 ($self->{'mutes'} !~ m/^(?:.*\s|)\Q$event->{'channel'}\E(?:|\s.*)$/si)) and # it was sent on a channel in which we aren't muted
207 (not $self->ignoringCommentsFrom($event->{'from'})) and # we aren't ignoring them
208 (not $self->ignoringCommentsTo($message))) { # and they aren't talking to someone we need to ignore
211 my $bugsToFetch = '';
215 if ($rest =~ m/ (?:^| # either the start of the string
216 []\s,.;:\\\/=?!()<>{}[-]) # or some punctuation
217 bug [\s\#]* ([0-9]+) # followed a string similar to "bug # 123" (put the number in $1)
218 (?:[]\s,.;:\\\/=?!()<>{}[-]+ # followed optionally by some punctuation,
219 (.*))?$/osix) { # and everything else (which we put in $2)
223 } elsif ($rest =~ m/\Q$self->{'bugsURI'}\Eshow_bug.cgi\?id=([0-9]+)(?:[^0-9&](.*))?$/si) {
231 $self->debug("Noticed someone mention bug $bug -- investigating...");
232 $bugsToFetch .= "$bug ";
235 } while (defined($bug));
236 if ($bugsToFetch ne '') {
237 $self->FetchBug($event, $bugsToFetch, 'bug', {'skipURI' => $skipURI, 'skipZaroo' =>1});
246 my ($event, $message) = @_;
247 unless ($self->CheckForBugs($event, $message)) {
248 return $self->SUPER::Heard(@_);
250 return 0; # we've dealt with it, no need to do anything else.
255 my ($event, $message) = @_;
256 if ($message =~ m/^\s*(...+?)\s+bugs\s*$/osi) {
257 my $target = $event->{'target'};
258 $self->FetchBug($event, $1, 'dwim');
260 return $self->SUPER::Baffled(@_);
267 my ($event, $message) = @_;
268 unless ($self->CheckForBugs($event, $message)) {
269 return $self->SUPER::Felt(@_);
271 return 0; # we've dealt with it, no need to do anything else.
276 my ($event, $message) = @_;
277 unless ($self->CheckForBugs($event, $message)) {
278 return $self->SUPER::Saw(@_);
280 return 0; # we've dealt with it, no need to do anything else.
285 my ($event, $bugParams, $subtype, $params) = @_;
286 my $skipURI = exists($params->{'skipURI'}) ? $params->{'skipURI'} : 0;
287 my $skipZaroo = exists($params->{'skipZaroo'}) ? $params->{'skipZaroo'} : 0;
288 my $sayAlways = exists($params->{'sayAlways'}) ? $params->{'sayAlways'} : 0;
291 my @bugs = split(' ', $bugParams);
293 foreach my $bug (@bugs) {
294 if($sayAlways || $self->needToFetchBug($event->{'target'}, $event->{'time'}, $bug)) {
296 $self->{'bugsHistory'}->{$event->{'target'}}->{$bug} = $event->{'time'} if $bug =~ m/^\d+$/os;
300 if ($subtype eq 'bug') {
301 # Code taken from Bugzilla's xml.cgi
302 $uri = "$self->{'bugsURI'}show_bug.cgi?ctype=xml&excludefield=long_desc&excludefield=attachmentdata&excludefield=cc".join('', map { $_ = "&id=" . $_ } @ids);
304 } elsif ($subtype eq 'dwim') {
305 # XXX should escape query string
306 my $DWIMdefaultQuery = $self->{'bugsDWIMQueryDefault'};
307 if (exists $self->{'bugsDWIMQueryChannelDefault'}->{$event->{'channel'}}) {
308 $DWIMdefaultQuery = $self->{'bugsDWIMQueryChannelDefault'}->{$event->{'channel'}};
310 $uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&$DWIMdefaultQuery".join(',',@ids);
314 $uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&bug_id=".join(',',@ids);
317 $self->getURI($event, $uri, $type, $subtype, $skipURI, $skipZaroo);
322 my ($event, $uri, $output, $type, $subtype, $skipURI, $skipZaroo) = @_;
326 # Bugzilla really needs a LIMIT option
328 if ($event->{'channel'}) {
335 if ($type eq 'buglist') {
336 # We asked for rdf, but old versions won't know how to do that
337 # So lets do some simple sniffing, until mozbot gives us a way
338 # to find out the server's returned mime type
340 if ($output =~ /^<\?xml /) {
350 if ($type eq 'html') {
355 { no warnings; # this can go _very_ wrong easily
357 $lots = ($output !~ m/<FORM\s+METHOD=POST\s+ACTION="long_list.cgi">/osi); # if we got truncated, then this will be missing
359 # Instead of relying on being able to accurately count the
360 # number of bugs (which we can't do if there are more than
361 # 199), use the number that bugzilla tells us.
362 if ($output =~ /(One|\d+) bugs? found/o) {
364 if ($bugCount eq "One") {
369 $output =~ s/<\/TABLE><TABLE .+?<\/A><\/TH>//gosi;
370 (undef, $output) = split(/Summary<\/A><\/TH>/osi, $output);
371 ($output, undef) = split(/<\/TABLE>/osi, $output);
372 $output =~ s/[\n\r]//gosi;
373 @qp = split(m/<TR VALIGN=TOP ALIGN=LEFT CLASS=[-A-Za-z0-9]+(?: style='.*?')?\s*?><TD>/osi, $output);
376 if (scalar(@qp) == 0) {
380 if (!$lots && $subtype eq 'bugs') {
381 if (scalar(@qp) > $maxRes) {
383 @qp = @qp[0..$maxRes-1];
389 if (my @d = m|<A HREF="show_bug.cgi\?id=([0-9]+)">\1</A> <td class=severity><nobr>(.*?)</nobr><td class=priority><nobr>(.*?)</nobr><td class=platform><nobr>(.*?)</nobr><td class=owner><nobr>(.*?)</nobr><td class=status><nobr>(.*?)</nobr><td class=resolution><nobr>(.*?)</nobr><td class=summary>(.*)|osi) {
390 # bugid severity priority platform owner status resolution subject
392 ($bug{'id'}, $bug{'severity'}, $bug{'priority'}, $bug{'platform'}, $bug{'owner'}, $bug{'status'}, $bug{'resolution'}, $bug{'summary'}) = @d;
398 } elsif ($type eq 'xml') {
399 # We came from xml.cgi
400 my $parser = XML::LibXML->new();
401 my $tree = $parser->parse_string($output);
402 my $root = $tree->getDocumentElement;
404 my @xml_bugs = $root->getElementsByTagName('bug');
405 $bugCount = scalar(@xml_bugs);
407 if (scalar(@xml_bugs) > $maxRes) {
409 @xml_bugs = @xml_bugs[0..$maxRes-1];
412 # OK, xml.cgi uses different names to the query stuff
413 # Take a deep breath, and use a mapping for the fields we
417 'bug_severity' => 'severity',
418 'priority' => 'priority',
419 'target_milestone' => 'target_milestone',
420 'assigned_to' => 'owner',
421 'bug_status' => 'status',
422 'resolution' => 'resolution',
423 'short_desc' => 'summary'
426 foreach my $xml_bug(@xml_bugs) {
428 my $error = $xml_bug->getAttribute('error');
429 if (!defined $error) {
430 foreach my $field (keys %fieldMap) {
431 my @arr = $xml_bug->getElementsByTagName($field);
433 my $firstChild = $arr[0]->getFirstChild();
434 if (defined $firstChild) {
435 $bug{$fieldMap{$field}} = $firstChild->getData();
441 my @arr = $xml_bug->getElementsByTagName('bug_id');
442 $bug{'id'} = $arr[0]->getFirstChild->getData();
443 $bug{'error'} = $error;
447 } elsif ($type eq 'rdf') {
448 my $parser = XML::LibXML->new();
449 my $tree = $parser->parse_string($output);
450 my $root = $tree->getDocumentElement;
451 my @rdf_bugs = $root->getElementsByTagName('bz:bug');
453 $bugCount = scalar(@rdf_bugs);
455 if (scalar(@rdf_bugs) > $maxRes) {
457 @rdf_bugs = @rdf_bugs[0..$maxRes-1];
460 foreach my $rdf_bug (@rdf_bugs) {
462 my @children = $rdf_bug->getChildnodes();
463 foreach my $child (@children) {
464 next if ($child->getLocalName() eq 'text');
465 my $field = $child->getLocalName();
466 if ($child->getFirstChild()) {
467 my $val = $child->getFirstChild->getData();
474 return $self->SUPER::GotURI(@_);
477 # construct the response's preamble
479 if ($bugCount == 0 && !$skipZaroo) {
480 $preamble = 'Zarro boogs found.';
484 $bugCountStr = "$bugCount bug" . ($bugCount == 1 ? '' : 's')
488 if ($subtype eq 'total') {
489 $self->say($event, $bugCountStr);
494 $preamble = $bugCountStr ? "$bugCountStr, which is too many for me to handle without running out of memory."
495 : 'Way too many bugs found. I gave up so as to not run out of memory.';
496 $preamble .= "$bugCountStr Try to narrow your search or something!";
498 } elsif ($subtype ne 'bug' && $bugCount > 1) {
499 $preamble = $bugCountStr;
501 if ($event->{'channel'}) {
502 $preamble .= '. Five shown, please message me for more.';
504 $preamble .= '. Will only show 20 results, please use the Bugzilla query form if you want more.';
511 if ( !$event->{'from'}
512 || grep {$_ eq $event->{'from'}} @{$self->{'skipPrefixFor'}} )
514 # they don't want to have the report prefixed with their name
517 $prefix = "$event->{'from'}: ";
521 $self->say($event, "$prefix$preamble");
524 my $bug_link = $skipURI ? "" : "$self->{'bugsURI'}show_bug.cgi?id=";
526 # now send out the output
527 foreach my $bug (@bugs) {
528 if (!defined $bug->{'error'}) {
529 # Bugzilla doesn't give the TM by default, and we can't
530 # change this without using cookies, which aren't supported
531 # by the mozbot API. Later versions allow us to use a query param
532 # but we can't detect that that was accepted, which would break
534 # xml.cgi gives us everything, so we can print this if we got
536 # Maybe the list of columns to display could be a var, one day, after
537 # installations from source before Dec 2001 are no longer supported,
538 # or we can pass cookies
539 $self->say($event, $prefix .
540 "Bug $bug_link$bug->{'id'} " .
541 substr($bug->{'severity'} || $bug->{'bug_severity'}, 0, 3) . ", " .
542 $bug->{'priority'} . ", " .
543 ($bug->{'target_milestone'} ? "$bug->{'target_milestone'}, " : "") .
544 ($bug->{'owner'} || $bug->{'assigned_to'}) . ", " .
545 substr($bug->{'status'} || $bug->{'bug_status'}, 0, 4) .
546 ($bug->{'resolution'} ? " " . $bug->{'resolution'} : "") . ", " .
547 substr($bug->{'summary'} || $bug->{'short_desc'} || $bug->{'short_short_desc'}, 0, 100));
548 } elsif ($bug->{'error'} eq 'NotFound') {
550 $self->say($event, $prefix . "Bug $bug->{'id'} was not found.");
552 } elsif ($bug->{'error'} eq 'NotPermitted') {
553 $self->say($event, $prefix . "Bug $bug_link$bug->{'id'} is not accessible");
556 $self->say($prefix . "Error accessing bug $bug->{'id'}: $bug->{'error'}");
562 sub CheckForBugMail {
566 my ($bug_log, $bug_file) = $self->GetBugLog();
569 if (defined $bug_log) {
570 # We need LOCK_EX because we're going to truncate it.
571 flock($bug_log, LOCK_EX);
572 @log_lines = $bug_log->getlines();
573 $bug_log->truncate(0)
574 or ($self->debug("Failed to truncate $bug_file: $!") && return);
575 flock($bug_log, LOCK_UN);
576 $bug_log->close() or $self->debug("Failed to close $bug_file: $!");
577 $self->debug("Read " . scalar(@log_lines) . " bugmail log lines.")
581 # We will have already output a more detailed error from GetBugLog.
582 $self->debug("CheckForBugMail Failed: Couldn't read bugmail log.");
586 # Hash to keep track of which channels we've mentioned which bug details
587 # in, so we don't spew the same bug details over and over.
590 foreach my $line (@log_lines) {
592 #$self->debug("Reading log line: $line");
593 my $sep = FIELD_SEPARATOR;
594 $line =~ /^(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.*)$sep(.*)$sep(.+)$/;
595 my ($bug_id, $product, $component, $who, $field, $old, $new, $message) =
596 ($1, $2, $3, $4, $5, $6, $7, $8);
598 # Skip this line if we never report anything for this field.
599 next if !grep($_ eq $field, @{$self->{'reportFields'}});
601 my @prod_mute_fields =
602 split(/\s*,\s*/, $self->{'productMuteFields'}->{$product});
604 # Don't report to these channels if this product is muted for this field.
605 push (@chan_list, $self->CreateChannelList($product, $component))
606 unless grep($_ eq $field, @prod_mute_fields);
608 if ($field eq 'Product') {
609 my @old_mute_fields =
610 split(/\s*,\s*/, $self->{'productMuteFields'}->{$old});
611 push(@chan_list, $self->CreateChannelList($old, $component))
612 unless grep($_ eq $field, @old_mute_fields);
614 elsif ($field eq 'Component') {
615 my @comp_mute_fields = @prod_mute_fields;
616 push(@comp_mute_fields,
617 ($self->{'productMuteFields'}->{$product. $sep . $component}));
618 # Don't report it if the product is muted for this field, or if
619 # this specific component is muted for this field.
620 push(@chan_list, $self->CreateChannelList($product, $old))
621 unless grep($_ eq $field, @comp_mute_fields);
623 # Enable Mozbot to report both product and component of new bugs.
624 if (grep(lc($_) eq 'all', @{$self->{'reportComponent'}}) ||
625 grep(lc($_) eq lc($product), @{$self->{'reportComponent'}}) ||
626 grep(lc($_) eq lc($product.$sep.$component), @{$self->{'reportComponent'}})) {
627 $message =~ s/^New $product bug/New $product - $component bug/i;
629 unless ($self->ignoringMailProducedBy($who)) {
630 # Keep track of which channels we've told already, to avoid
631 # duplicate messages.
633 foreach my $channel (@chan_list) {
634 my @chan_mute_fields =
635 split(/\s*,\s*/, $self->{'channelMuteFields'}->{$channel});
636 # Don't say it if we've said it before, or if this
637 # field is muted in this channel.
638 unless ( $said_to{$channel}
639 || grep($_ eq $field, @chan_mute_fields) )
641 # We can't use "local" here, or the target doesn't show
642 # up properly in the GotURI after FetchBug.
643 $event->{'target'} = $channel;
644 $self->say($event, $message);
646 # Special case for "duplicate of messages"
647 if ($message =~ /DUPLICATE of bug (\d+)/) {
649 $bugids = $dup_id unless $said_bug{$channel . $dup_id};
650 $said_bug{$channel . $dup_id} = 1;
652 # Fetch bugs mentioned for dependent field changes
653 if ($field eq 'OtherBugsDependingOnThis'
654 || $field eq 'BugsThisDependsOn') {
655 foreach my $id (split(/,/, $old . $new)) {
656 $bugids = $id . " " . $bugids
657 unless $said_bug{$channel . $id};
658 $said_bug{$channel . $id} = 1;
661 if (! $said_bug{$channel . $bug_id}) {
662 $bugids = $bug_id . " " . $bugids;
665 if ($self->{'reportBugDetails'}) {
666 $self->FetchBug($event, $bugids, 'bug');
669 $said_to{$channel} = 1;
670 $said_bug{$channel . $bug_id} = 1;
672 } # foreach @chan_list
673 } # unless ignoringMailProducedBy
674 } # foreach @log_lines
677 # A helper for CheckForBugMail.
678 sub CreateChannelList {
680 my ($product, $component) = @_;
683 ($chan_list .= $self->{'productReportChannels'}->{$product})
684 if $self->{'productReportChannels'}->{$product};
686 my $prodcomp = $product . FIELD_SEPARATOR . $component;
687 ($chan_list .= ',' . $self->{'productReportChannels'}->{$prodcomp})
688 if $self->{'productReportChannels'}->{$prodcomp};
690 return (split /\s*,\s*/, $chan_list);
693 # Creates the BUGMAIL_LOG file if it doesn't exist, and returns
694 # an open IO::File for it, and also the filename of that file.
698 my $file_name = dirname($0) . '/' . BUGMAIL_LOG;
699 # And we generally trust $bug_log to be an OK path, so untaint it now.
700 $file_name =~ /^(.*)$/;
702 my $file = new IO::File($file_name, O_RDWR | O_CREAT, 0660)
703 or $self->debug("Could not open/create $file_name for reading"
704 . " incoming bugmail: $!");
705 return ($file, $file_name);
708 sub ignoringMailProducedBy {
711 return grep($_ eq $who, @{$self->{'mailIgnore'}}) ? 1 : 0;
714 sub ignoringCommentsTo {
717 foreach (@{$self->{'ignoreCommentsTo'}}) {
718 next unless $_; # Ignore blanks, happens when the array is empty (?)
719 return 1 if $who =~ m/^(?:.*[]\s,.;:\\\/=?!()<>{}[-])?\Q$_\E(?:[]\s,.;:\\\/=?!()<>{}[-].*)?$/is;
724 sub ignoringCommentsFrom {
727 foreach (@{$self->{'ignoreCommentsFrom'}}) {
728 return 1 if $_ eq $who;
734 my ($self, $target, $time, $bug) = @_;
736 if (defined($self->{'bugsHistory'}->{$target}->{$bug})) {
737 $last = $self->{'bugsHistory'}->{$target}->{$bug};
739 if (($time-$last) > $self->{'backoffTime'}) {