From owner-svn-src-user@FreeBSD.ORG Wed Jan 22 08:33:34 2014 Return-Path: Delivered-To: svn-src-user@freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [8.8.178.115]) (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) (No client certificate requested) by hub.freebsd.org (Postfix) with ESMTPS id 24E686B; Wed, 22 Jan 2014 08:33:34 +0000 (UTC) Received: from svn.freebsd.org (svn.freebsd.org [IPv6:2001:1900:2254:2068::e6a:0]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mx1.freebsd.org (Postfix) with ESMTPS id 1064B1E97; Wed, 22 Jan 2014 08:33:34 +0000 (UTC) Received: from svn.freebsd.org ([127.0.1.70]) by svn.freebsd.org (8.14.7/8.14.7) with ESMTP id s0M8XYnC039444; Wed, 22 Jan 2014 08:33:34 GMT (envelope-from gonzo@svn.freebsd.org) Received: (from gonzo@localhost) by svn.freebsd.org (8.14.7/8.14.7/Submit) id s0M8XW7v039435; Wed, 22 Jan 2014 08:33:32 GMT (envelope-from gonzo@svn.freebsd.org) Message-Id: <201401220833.s0M8XW7v039435@svn.freebsd.org> From: Oleksandr Tymoshenko Date: Wed, 22 Jan 2014 08:33:32 +0000 (UTC) To: src-committers@freebsd.org, svn-src-user@freebsd.org Subject: svn commit: r261008 - in user/gonzo/bugzilla-freebsd: . AutoAssigner X-SVN-Group: user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-BeenThere: svn-src-user@freebsd.org X-Mailman-Version: 2.1.17 Precedence: list List-Id: "SVN commit messages for the experimental " user" src tree" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 22 Jan 2014 08:33:34 -0000 Author: gonzo Date: Wed Jan 22 08:33:32 2014 New Revision: 261008 URL: http://svnweb.freebsd.org/changeset/base/261008 Log: Import bugzilla/freebsd integration stuff Added: user/gonzo/bugzilla-freebsd/ user/gonzo/bugzilla-freebsd/AutoAssigner/ user/gonzo/bugzilla-freebsd/AutoAssigner/Config.pm (contents, props changed) user/gonzo/bugzilla-freebsd/AutoAssigner/Extension.pm (contents, props changed) user/gonzo/bugzilla-freebsd/README user/gonzo/bugzilla-freebsd/gnats_in.pl (contents, props changed) user/gonzo/bugzilla-freebsd/import_ports_index.pl (contents, props changed) user/gonzo/bugzilla-freebsd/svn_in.pl (contents, props changed) Added: user/gonzo/bugzilla-freebsd/AutoAssigner/Config.pm ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ user/gonzo/bugzilla-freebsd/AutoAssigner/Config.pm Wed Jan 22 08:33:32 2014 (r261008) @@ -0,0 +1,12 @@ +package Bugzilla::Extension::AutoAssigner; +use strict; +use constant NAME => 'AutoAssigner'; +use constant REQUIRED_MODULES => [ + { + package => 'Data-Dumper', + module => 'Data::Dumper', + version => 0, + }, +]; + +__PACKAGE__->NAME; Added: user/gonzo/bugzilla-freebsd/AutoAssigner/Extension.pm ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ user/gonzo/bugzilla-freebsd/AutoAssigner/Extension.pm Wed Jan 22 08:33:32 2014 (r261008) @@ -0,0 +1,66 @@ +package Bugzilla::Extension::AutoAssigner; + +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::User; +use Bugzilla::User::Setting; +use Bugzilla::Util qw(diff_arrays html_quote); +use Bugzilla::Status qw(is_open_state); +use Bugzilla::Install::Filesystem; + +use constant REL_EXAMPLE => -127; + +our $VERSION = '1.0'; + +use Data::Dumper; + +sub bug_end_of_create { + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; + + my $bug_id = $bug->id; + my $subject = $bug->short_desc; + my $component = $bug->component; + if ($component eq 'ports') { + if ($subject =~ /^\s*(\S+\/\S+)\s*:/) { + my $port = $1; + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare("SELECT maintainer FROM freebsd_ports_index where port=?"); + $sth->execute($port); + my ($maintainer) = $sth->fetchrow_array(); + return unless(defined($maintainer)); + eval { + my $user = Bugzilla::User->check($maintainer); + }; + return if ($@ ne ''); + $bug->add_cc($maintainer); + $bug->update(); + } + } +} + +sub db_schema_abstract_schema { + my ($class, $args) = @_; + my $schema = $args->{schema}; + + $schema->{freebsd_ports_index} = { + FIELDS => [ + port => {TYPE => 'varchar(255)', NOTNULL => 1}, + maintainer => {TYPE => 'varchar(255)', NOTNULL => 1}, + ], + INDEXES => [ + freebsd_ports_index_port_id => { FIELDS => ['port'], TYPE => 'UNIQUE' }, + ], + }; +} + +# This must be the last line of your extension. +__PACKAGE__->NAME; Added: user/gonzo/bugzilla-freebsd/README ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ user/gonzo/bugzilla-freebsd/README Wed Jan 22 08:33:32 2014 (r261008) @@ -0,0 +1,10 @@ +AutoAssigner - add port maintainer to Cc for newly created bugs in + ports category. Summary of the bug should have "category/port: " + prefix. Extension creates table for INDEX file content. File + itself is imported by import_ports_index.pl + +gnats_in.pl - script that parses emails generated by submit-pr and + creates new bug in Bugzilla. + +subversion_in.pl - Very basic dfilter-like script. Gets sendmail + commit email on input, parses and posts comment to bug mentioned Added: user/gonzo/bugzilla-freebsd/gnats_in.pl ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ user/gonzo/bugzilla-freebsd/gnats_in.pl Wed Jan 22 08:33:32 2014 (r261008) @@ -0,0 +1,454 @@ +#!/usr/local/bin/perl -w +# -*- 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 Inbound Email System. +# +# The Initial Developer of the Original Code is Akamai Technologies, Inc. +# Portions created by Akamai are Copyright (C) 2006 Akamai Technologies, +# Inc. All Rights Reserved. +# +# Contributor(s): Max Kanat-Alexander + +use strict; +use warnings; + +# MTAs may call this script from any directory, but it should always +# run from this one so that it can find its modules. +use Cwd qw(abs_path); +use File::Basename qw(dirname); +BEGIN { + # Untaint the abs_path. + my ($a) = abs_path($0) =~ /^(.*)$/; + chdir dirname($a); +} + +use lib qw(/usr/local/www/bugs42.freebsd.org/lib/ /usr/local/www/bugs42.freebsd.org/); + +use Data::Dumper; +use Email::Address; +use Email::Reply qw(reply); +use Email::MIME; +use Email::MIME::Attachment::Stripper; +use Getopt::Long qw(:config bundling); +use Pod::Usage; +use Encode; +use Scalar::Util qw(blessed); + +use Bugzilla; +use Bugzilla::Attachment; +use Bugzilla::Bug; +use Bugzilla::BugMail; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Mailer; +use Bugzilla::Token; +use Bugzilla::User; +use Bugzilla::Util; +use Bugzilla::Hook; + +############# +# Constants # +############# + + +our %fields_defaults = ( + op_sys => 'FreeBSD', + rep_platform => 'old_All', + product => 'FreeBSD', + version => 'unspecified', +); + +our %priority_map = ( + medium => 'normal' +); + +our %severity_map = ( + 'non-critical' => 'normal', + critical => 'old critical', + serious => 'normal' +); + +# $input_email is a global so that it can be used in die_handler. +our ($input_email, %switch); + +#################### +# Main Subroutines # +#################### + +sub parse_mail { + my ($mail_text) = @_; + debug_print('Parsing Email'); + $input_email = Email::MIME->new($mail_text); + + my %fields = %{ $switch{'default'} || {} }; + Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, + fields => \%fields }); + + my $body = get_body($input_email); + + debug_print("Body:\n" . $body, 3); + + my @body_lines = split(/\r?\n/s, $body); + + # If there are fields specified. + my %gnats_fields; + my $current_field; + foreach my $line (@body_lines) { + if ($line =~ /^>([\w-]+):\s*(.*)\s*/) { + $current_field = lc($1); + $gnats_fields{$current_field} = $2; + } + else { + $gnats_fields{$current_field} .= "\n$line"; + } + } + + foreach my $k (keys %gnats_fields) { + $gnats_fields{$k} = trim ($gnats_fields{$k}); + } + + debug_print("GNATS Fields:\n" . Dumper(\%gnats_fields), 2); + $fields{priority} = $priority_map{$gnats_fields{priority}} || $gnats_fields{priority}; + $fields{severity} = $severity_map{$gnats_fields{severity}} || $gnats_fields{severity}; + $fields{component} = $gnats_fields{category}; + # $fields{cf_type} = $gnats_fields{class}; + + %fields = %{ Bugzilla::Bug::map_fields(\%fields) }; + + my ($reporter) = Email::Address->parse($input_email->header('From')); + $fields{'reporter'} = $reporter->address; + # $fields{'cf_originator_email'} = $reporter->address; + $fields{'reporter'} = 'gonzo@freebsd.org'; + + # Default values + foreach my $k (keys %fields_defaults) { + $fields{$k} = $fields_defaults{$k}; + } + + my $subject = $input_email->header('Subject'); + $fields{'short_desc'} = $gnats_fields{'synopsis'} || $subject; + + my $comment = ''; + + + if (defined($gnats_fields{'description'}) && $gnats_fields{'description'} =~ /\S/s) { + $comment .= $gnats_fields{'description'}; + } + + if (defined($gnats_fields{'environment'}) && $gnats_fields{'environment'} =~ /\S/s) { + my $envstr = $gnats_fields{'environment'}; + if ($envstr =~ /(?:FreeBSD )?([0-9]+\.[0-9\.]+-((PRE)?RELEASE|BETA[0-9]|CURRENT|STABLE))(-p\d+)?( [a-z0-9]+)?/) { + my $version_name = $1; + my $product = Bugzilla::Product->check($fields_defaults{product}); + eval { + my $version_obj = Bugzilla::Version->check({ product => $product, + name => $version_name }); + }; + if ($@ eq '') { + $fields{'version'} = $version_name; + } + } + $comment .= "\n\nEnvironment:\n"; + $comment .= $gnats_fields{'environment'}; + } + + if (defined($gnats_fields{'how-to-repeat'}) && $gnats_fields{'how-to-repeat'} =~ /\S/s) { + $comment .= "\n\nHow-To-Repeat:\n"; + $comment .= $gnats_fields{'how-to-repeat'}; + } + + if (defined($gnats_fields{'fix'})) { + my $attachments; + my $fix = $gnats_fields{'fix'}; + ($fix, $attachments) = parse_fix($fix); + if ($fix =~ /\S/s) { + $comment .= "\n\nFix:\n"; + $comment .= $fix; + } + + $fields{'attachments'} = $attachments if (@$attachments); + } + + $fields{'comment'} = $comment; + + debug_print("Parsed Fields:\n" . Dumper(\%fields), 2); + + return \%fields; +} + +sub post_bug { + my ($fields) = @_; + debug_print('Posting a new bug...'); + + my $user = Bugzilla->user; + + my ($retval, $non_conclusive_fields) = + Bugzilla::User::match_field({ + 'assigned_to' => { 'type' => 'single' }, + 'qa_contact' => { 'type' => 'single' }, + 'cc' => { 'type' => 'multi' } + }, $fields, MATCH_SKIP_CONFIRM); + + if ($retval != USER_MATCH_SUCCESS) { + ThrowUserError('user_match_too_many', {fields => $non_conclusive_fields}); + } + + my $bug = Bugzilla::Bug->create($fields); + debug_print("Created bug " . $bug->id); + return ($bug, $bug->comments->[0]); +} + +sub handle_attachments { + my ($bug, $attachments, $comment) = @_; + return if !$attachments; + debug_print("Handling attachments..."); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my ($update_comment, $update_bug); + foreach my $attachment (@$attachments) { + my $data = delete $attachment->{payload}; + debug_print("Inserting Attachment: " . Dumper($attachment), 2); + $attachment->{content_type} ||= 'application/octet-stream'; + my $ispatch = 0; + $ispatch = 1 if ($attachment->{filename} =~ /(diff|patch)$/); + my $obj = Bugzilla::Attachment->create({ + bug => $bug, + description => $attachment->{filename}, + filename => $attachment->{filename}, + mimetype => $attachment->{content_type}, + ispatch => $ispatch, + data => $data, + }); + # If we added a comment, and our comment does not already have a type, + # and this is our first attachment, then we make the comment an + # "attachment created" comment. + if ($comment and !$comment->type and !$update_comment) { + $comment->set_all({ type => CMT_ATTACHMENT_CREATED, + extra_data => $obj->id }); + $update_comment = 1; + } + else { + $bug->add_comment('', { type => CMT_ATTACHMENT_CREATED, + extra_data => $obj->id }); + $update_bug = 1; + } + } + # We only update the comments and bugs at the end of the transaction, + # because doing so modifies bugs_fulltext, which is a non-transactional + # table. + $bug->update() if $update_bug; + $comment->update() if $update_comment; + $dbh->bz_commit_transaction(); +} + +###################### +# Helper Subroutines # +###################### + +sub debug_print { + my ($str, $level) = @_; + $level ||= 1; + print STDERR "$str\n" if $level <= $switch{'verbose'}; +} + +sub get_body { + my ($email) = @_; + + my $ct = $email->content_type || 'text/plain'; + debug_print("Splitting Body and Attachments [Type: $ct]..."); + + my $body; + if ($ct =~ /^multipart\/(alternative|signed)/i) { + $body = get_text_alternative($email); + } + else { + my $stripper = new Email::MIME::Attachment::Stripper( + $email, force_filename => 1); + my $message = $stripper->message; + $body = get_text_alternative($message); + } + + return $body; +} + +sub parse_fix { + my ($fix) = @_; + my $stripped_fix = ''; + my $attachments = []; + my $attachment; + my @fix_lines = split(/\r?\n/s, $fix); + foreach my $line (@fix_lines) { + if ($line =~ /^\s*--{1,8}\s?([A-Za-z0-9-_.,:%]+) (begins|starts) here\s?--+\s*/mi) { + push @$attachments, $attachment if (defined($attachment)); + $attachment = { + 'payload' => '', + 'filename' => $1, + 'content_type' => 'text/plain', + }; + } + elsif ($line =~ /^\s*--{1,8}\s?([A-Za-z0-9-_.,:%]+) ends here\s?--+\s*\n/mi) { + push @$attachments, $attachment if (defined($attachment)); + $attachment = undef; + } + elsif ($line =~ /^# This is a shell archive/) { + push @$attachments, $attachment if (defined($attachment)); + $attachment = { + 'payload' => "$line\n", + 'filename' => 'file.shar', + 'content_type' => 'application/x-shar', + }; + } + elsif (($line =~ /^exit$/) && defined($attachment) && ($attachment->{content_type} =~/x-shar/)) { + $attachment->{'payload'} .= "$line\n"; + push @$attachments, $attachment if (defined($attachment)); + $attachment = undef; + } + + elsif (defined($attachment)) { + $attachment->{'payload'} .= "$line\n"; + } + else { + $stripped_fix .= "$line\n"; + } + } + push @$attachments, $attachment if (defined($attachment)); + return ($stripped_fix, $attachments) +} + +sub get_text_alternative { + my ($email) = @_; + + my @parts = $email->parts; + my $body; + foreach my $part (@parts) { + my $ct = $part->content_type || 'text/plain'; + my $charset = 'iso-8859-1'; + # The charset may be quoted. + if ($ct =~ /charset="?([^;"]+)/) { + $charset= $1; + } + debug_print("Part Content-Type: $ct", 2); + debug_print("Part Character Encoding: $charset", 2); + if (!$ct || $ct =~ /^text\/plain/i) { + $body = $part->body; + if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($body)) { + $body = Encode::decode($charset, $body); + } + last; + } + } + + if (!defined $body) { + # Note that this only happens if the email does not contain any + # text/plain parts. If the email has an empty text/plain part, + # you're fine, and this message does NOT get thrown. + ThrowUserError('email_no_text_plain'); + } + + return $body; +} + +sub html_strip { + my ($var) = @_; + # Trivial HTML tag remover (this is just for error messages, really.) + $var =~ s/<[^>]*>//g; + # And this basically reverses the Template-Toolkit html filter. + $var =~ s/\&/\&/g; + $var =~ s/\<//g; + $var =~ s/\"/\"/g; + $var =~ s/@/@/g; + # Also remove undesired newlines and consecutive spaces. + $var =~ s/[\n\s]+/ /gms; + return $var; +} + +sub die_handler { + my ($msg) = @_; + + # In Template-Toolkit, [% RETURN %] is implemented as a call to "die". + # But of course, we really don't want to actually *die* just because + # the user-error or code-error template ended. So we don't really die. + return if blessed($msg) && $msg->isa('Template::Exception') + && $msg->type eq 'return'; + + # If this is inside an eval, then we should just act like...we're + # in an eval (instead of printing the error and exiting). + die(@_) if $^S; + + # We can't depend on the MTA to send an error message, so we have + # to generate one properly. + if ($input_email) { + $msg =~ s/at .+ line.*$//ms; + $msg =~ s/^Compilation failed in require.+$//ms; + $msg = html_strip($msg); + my $from = Bugzilla->params->{'mailfrom'}; + my $reply = reply(to => $input_email, from => $from, top_post => 1, + body => "$msg\n"); + # MessageToMTA($reply->as_string); + } + print STDERR "$msg\n"; + # We exit with a successful value, because we don't want the MTA + # to *also* send a failure notice. + exit; +} + +############### +# Main Script # +############### + +$SIG{__DIE__} = \&die_handler; + +GetOptions(\%switch, 'help|h', 'verbose|v+', 'default=s%', 'override=s%'); +$switch{'verbose'} ||= 0; + +# Print the help message if that switch was selected. +pod2usage({-verbose => 0, -exitval => 1}) if $switch{'help'}; + +Bugzilla->usage_mode(USAGE_MODE_EMAIL); + +my @mail_lines = ; +my $mail_text = join("", @mail_lines); +my $mail_fields = parse_mail($mail_text); + +Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields }); + +my $attachments = delete $mail_fields->{'attachments'}; + +my $username = $mail_fields->{'reporter'}; +# If emailsuffix is in use, we have to remove it from the email address. +if (my $suffix = Bugzilla->params->{'emailsuffix'}) { + $username =~ s/\Q$suffix\E$//i; +} + +my $user = Bugzilla::User->check($username); +Bugzilla->set_user($user); + +my ($bug, $comment); +($bug, $comment) = post_bug($mail_fields); + +handle_attachments($bug, $attachments, $comment); + +# This is here for post_bug and handle_attachments, so that when posting a bug +# with an attachment, any comment goes out as an attachment comment. +# +# Eventually this should be sending the mail for process_bug, too, but we have +# to wait for $bug->update() to be fully used in email_in.pl first. So +# currently, process_bug.cgi does the mail sending for bugs, and this does +# any mail sending for attachments after the first one. +Bugzilla::BugMail::Send($bug->id, { changer => Bugzilla->user }); +debug_print("Sent bugmail"); +print "--> " . $bug->id . "\n"; + + +__END__ Added: user/gonzo/bugzilla-freebsd/import_ports_index.pl ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ user/gonzo/bugzilla-freebsd/import_ports_index.pl Wed Jan 22 08:33:32 2014 (r261008) @@ -0,0 +1,29 @@ +#!/usr/local/bin/perl -w + +use strict; +use warnings; + +use lib qw(/usr/local/www/bugs42.freebsd.org/lib/ /usr/local/www/bugs42.freebsd.org/); +use Bugzilla; + +open F, "< /var/ports/INDEX-9"; + +my %maintainers; + +while() { + chomp; + my @fields = split /\|/; + my $port = $fields[1]; + $port =~ s@/usr/ports/@@; + my $maintainer = $fields[5]; + $maintainers{$port} = $maintainer; +} + +my $dbh = Bugzilla->dbh; +$dbh->bz_start_transaction(); +$dbh->do("DELETE from freebsd_ports_index"); +my $sth = $dbh->prepare("insert into freebsd_ports_index values (?, ?)"); +foreach my $k (keys %maintainers) { + $sth->execute($k, $maintainers{$k}); +} +$dbh->bz_commit_transaction(); Added: user/gonzo/bugzilla-freebsd/svn_in.pl ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ user/gonzo/bugzilla-freebsd/svn_in.pl Wed Jan 22 08:33:32 2014 (r261008) @@ -0,0 +1,284 @@ +#!/usr/local/bin/perl -w +# -*- 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 Inbound Email System. +# +# The Initial Developer of the Original Code is Akamai Technologies, Inc. +# Portions created by Akamai are Copyright (C) 2006 Akamai Technologies, +# Inc. All Rights Reserved. +# +# Contributor(s): Max Kanat-Alexander + +use strict; +use warnings; + +# MTAs may call this script from any directory, but it should always +# run from this one so that it can find its modules. +use Cwd qw(abs_path); +use File::Basename qw(dirname); +BEGIN { + # Untaint the abs_path. + my ($a) = abs_path($0) =~ /^(.*)$/; + chdir dirname($a); +} + +use lib qw(/usr/local/www/bugs42.freebsd.org/lib/ /usr/local/www/bugs42.freebsd.org/); + +use Data::Dumper; +use Email::Address; +use Email::Reply qw(reply); +use Email::MIME; +use Email::MIME::Attachment::Stripper; +use Getopt::Long qw(:config bundling); +use Pod::Usage; +use Encode; +use Scalar::Util qw(blessed); + +use Bugzilla; +use Bugzilla::Attachment; +use Bugzilla::Bug; +use Bugzilla::BugMail; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Mailer; +use Bugzilla::Token; +use Bugzilla::User; +use Bugzilla::Util; +use Bugzilla::Hook; + +# $input_email is a global so that it can be used in die_handler. +our ($input_email, %switch); + +#################### +# Main Subroutines # +#################### + +sub parse_mail { + my ($mail_text) = @_; + debug_print('Parsing Email'); + $input_email = Email::MIME->new($mail_text); + + my %fields = %{ $switch{'default'} || {} }; + Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, + fields => \%fields }); + + my $body = get_body($input_email); + + # debug_print("Body:\n" . $body, 3); + + my @body_lines = split(/\r?\n/s, $body); + + my @comment_lines = (); + foreach my $l (@body_lines) { + last if ($l =~ /^Modified:.*\/.*/); + push @comment_lines, $l; + } + my $comment = join "\n", @comment_lines; + $fields{'comment'} = $comment; + if ($comment =~ /\n\s*PR:\s*(?:\w+\/)?(\d+)/) { + $fields{'bug_id'} = $1; + } + + debug_print("Parsed Fields:\n" . Dumper(\%fields), 2); + + return \%fields; +} + +###################### +# Helper Subroutines # +###################### + +sub debug_print { + my ($str, $level) = @_; + $level ||= 1; + print STDERR "$str\n" if $level <= $switch{'verbose'}; +} + +sub get_body { + my ($email) = @_; + + my $ct = $email->content_type || 'text/plain'; + debug_print("Splitting Body and Attachments [Type: $ct]..."); + + my $body; + if ($ct =~ /^multipart\/(alternative|signed)/i) { + $body = get_text_alternative($email); + } + else { + my $stripper = new Email::MIME::Attachment::Stripper( + $email, force_filename => 1); + my $message = $stripper->message; + $body = get_text_alternative($message); + } + + return $body; +} + +sub parse_fix { + my ($fix) = @_; + my $stripped_fix = ''; + my $attachments = []; + my $attachment; + my @fix_lines = split(/\r?\n/s, $fix); + foreach my $line (@fix_lines) { + if ($line =~ /^\s*--{1,8}\s?([A-Za-z0-9-_.,:%]+) (begins|starts) here\s?--+\s*/mi) { + push @$attachments, $attachment if (defined($attachment)); + $attachment = { + 'payload' => '', + 'filename' => $1, + 'content_type' => 'text/plain', + }; + } + elsif ($line =~ /^\s*--{1,8}\s?([A-Za-z0-9-_.,:%]+) ends here\s?--+\s*\n/mi) { + push @$attachments, $attachment if (defined($attachment)); + $attachment = undef; + } + elsif ($line =~ /^# This is a shell archive/) { + push @$attachments, $attachment if (defined($attachment)); + $attachment = { + 'payload' => "$line\n", + 'filename' => 'file.shar', + 'content_type' => 'application/x-shar', + }; + } + elsif (($line =~ /^exit$/) && defined($attachment) && ($attachment->{content_type} =~/x-shar/)) { + $attachment->{'payload'} .= "$line\n"; + push @$attachments, $attachment if (defined($attachment)); + $attachment = undef; + } + + elsif (defined($attachment)) { + $attachment->{'payload'} .= "$line\n"; + } + else { + $stripped_fix .= "$line\n"; + } + } + push @$attachments, $attachment if (defined($attachment)); + return ($stripped_fix, $attachments) +} + +sub get_text_alternative { + my ($email) = @_; + + my @parts = $email->parts; + my $body; + foreach my $part (@parts) { + my $ct = $part->content_type || 'text/plain'; + my $charset = 'iso-8859-1'; + # The charset may be quoted. + if ($ct =~ /charset="?([^;"]+)/) { + $charset= $1; + } + debug_print("Part Content-Type: $ct", 2); + debug_print("Part Character Encoding: $charset", 2); + if (!$ct || $ct =~ /^text\/plain/i) { + $body = $part->body; + if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($body)) { + $body = Encode::decode($charset, $body); + } + last; + } + } + + if (!defined $body) { + # Note that this only happens if the email does not contain any + # text/plain parts. If the email has an empty text/plain part, + # you're fine, and this message does NOT get thrown. + ThrowUserError('email_no_text_plain'); + } + + return $body; +} + +sub html_strip { + my ($var) = @_; + # Trivial HTML tag remover (this is just for error messages, really.) + $var =~ s/<[^>]*>//g; + # And this basically reverses the Template-Toolkit html filter. + $var =~ s/\&/\&/g; + $var =~ s/\<//g; + $var =~ s/\"/\"/g; + $var =~ s/@/@/g; + # Also remove undesired newlines and consecutive spaces. + $var =~ s/[\n\s]+/ /gms; + return $var; +} + +sub die_handler { + my ($msg) = @_; + + # In Template-Toolkit, [% RETURN %] is implemented as a call to "die". + # But of course, we really don't want to actually *die* just because + # the user-error or code-error template ended. So we don't really die. + return if blessed($msg) && $msg->isa('Template::Exception') + && $msg->type eq 'return'; + + # If this is inside an eval, then we should just act like...we're + # in an eval (instead of printing the error and exiting). + die(@_) if $^S; + + # We can't depend on the MTA to send an error message, so we have + # to generate one properly. + if ($input_email) { + $msg =~ s/at .+ line.*$//ms; + $msg =~ s/^Compilation failed in require.+$//ms; + $msg = html_strip($msg); + my $from = Bugzilla->params->{'mailfrom'}; + my $reply = reply(to => $input_email, from => $from, top_post => 1, + body => "$msg\n"); + # MessageToMTA($reply->as_string); + } + print STDERR "$msg\n"; + # We exit with a successful value, because we don't want the MTA + # to *also* send a failure notice. + exit; +} + +############### +# Main Script # +############### + +$SIG{__DIE__} = \&die_handler; + +GetOptions(\%switch, 'help|h', 'verbose|v+', 'default=s%', 'override=s%'); +$switch{'verbose'} ||= 0; + +# Print the help message if that switch was selected. +pod2usage({-verbose => 0, -exitval => 1}) if $switch{'help'}; + +Bugzilla->usage_mode(USAGE_MODE_EMAIL); + +my @mail_lines = ; +my $mail_text = join("", @mail_lines); +my $mail_fields = parse_mail($mail_text); + +exit(0) unless(defined($mail_fields->{'bug_id'})); + +my $bug_id = $mail_fields->{'bug_id'}; +my $comment = $mail_fields->{'comment'}; +my $bug = Bugzilla::Bug->check($bug_id); + +my $username = 'dfilter@freebsd.org'; +my $user = Bugzilla::User->check($username); +Bugzilla->set_user($user); + +$bug->add_comment($comment); +my $dbh = Bugzilla->dbh; +$dbh->bz_start_transaction(); + +$bug->update(); +$dbh->bz_commit_transaction(); + +__END__