1#!/usr/local/bin/perl -T 2# This Source Code Form is subject to the terms of the Mozilla Public 3# License, v. 2.0. If a copy of the MPL was not distributed with this 4# file, You can obtain one at http://mozilla.org/MPL/2.0/. 5# 6# This Source Code Form is "Incompatible With Secondary Licenses", as 7# defined by the Mozilla Public License, v. 2.0. 8 9use 5.10.1; 10use strict; 11use warnings; 12 13# MTAs may call this script from any directory, but it should always 14# run from this one so that it can find its modules. 15use Cwd qw(abs_path); 16use File::Basename qw(dirname); 17BEGIN { 18 # Untaint the abs_path. 19 my ($a) = abs_path($0) =~ /^(.*)$/; 20 chdir dirname($a); 21} 22 23use lib qw(. lib); 24 25use Data::Dumper; 26use Email::Address; 27use Email::Reply qw(reply); 28use Email::MIME; 29use Getopt::Long qw(:config bundling); 30use HTML::FormatText::WithLinks; 31use Pod::Usage; 32use Encode; 33use Scalar::Util qw(blessed); 34use List::MoreUtils qw(firstidx); 35 36use Bugzilla; 37use Bugzilla::Attachment; 38use Bugzilla::Bug; 39use Bugzilla::BugMail; 40use Bugzilla::Constants; 41use Bugzilla::Error; 42use Bugzilla::Field; 43use Bugzilla::Mailer; 44use Bugzilla::Token; 45use Bugzilla::User; 46use Bugzilla::Util; 47use Bugzilla::Hook; 48 49############# 50# Constants # 51############# 52 53# This is the USENET standard line for beginning a signature block 54# in a message. RFC-compliant mailers use this. 55use constant SIGNATURE_DELIMITER => '-- '; 56 57# These MIME types represent a "body" of an email if they have an 58# "inline" Content-Disposition (or no content disposition). 59use constant BODY_TYPES => qw( 60 text/plain 61 text/html 62 application/xhtml+xml 63 multipart/alternative 64); 65 66# $input_email is a global so that it can be used in die_handler. 67our ($input_email, %switch); 68 69#################### 70# Main Subroutines # 71#################### 72 73sub parse_mail { 74 my ($mail_text) = @_; 75 debug_print('Parsing Email'); 76 $input_email = Email::MIME->new($mail_text); 77 78 my %fields = %{ $switch{'default'} || {} }; 79 Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, 80 fields => \%fields }); 81 82 my $summary = $input_email->header('Subject'); 83 if ($summary =~ /\[\S+ (\d+)\](.*)/i) { 84 $fields{'bug_id'} = $1; 85 $summary = trim($2); 86 } 87 88 # Ignore automatic replies. 89 # XXX - Improve the way to detect such subjects in different languages. 90 my $auto_submitted = $input_email->header('Auto-Submitted') || ''; 91 if ($summary =~ /out of( the)? office/i || $auto_submitted eq 'auto-replied') { 92 debug_print("Automatic reply detected: $summary"); 93 exit; 94 } 95 96 my ($body, $attachments) = get_body_and_attachments($input_email); 97 98 debug_print("Body:\n" . $body, 3); 99 100 $body = remove_leading_blank_lines($body); 101 my @body_lines = split(/\r?\n/s, $body); 102 103 # If there are fields specified. 104 if ($body =~ /^\s*@/s) { 105 my $current_field; 106 while (my $line = shift @body_lines) { 107 # If the sig is starting, we want to keep this in the 108 # @body_lines so that we don't keep the sig as part of the 109 # comment down below. 110 if ($line eq SIGNATURE_DELIMITER) { 111 unshift(@body_lines, $line); 112 last; 113 } 114 # Otherwise, we stop parsing fields on the first blank line. 115 $line = trim($line); 116 last if !$line; 117 if ($line =~ /^\@(\w+)\s*(?:=|\s|$)\s*(.*)\s*/) { 118 $current_field = lc($1); 119 $fields{$current_field} = $2; 120 } 121 else { 122 $fields{$current_field} .= " $line"; 123 } 124 } 125 } 126 127 %fields = %{ Bugzilla::Bug::map_fields(\%fields) }; 128 129 my ($reporter) = Email::Address->parse($input_email->header('From')); 130 $fields{'reporter'} = $reporter->address; 131 132 # The summary line only affects us if we're doing a post_bug. 133 # We have to check it down here because there might have been 134 # a bug_id specified in the body of the email. 135 if (!$fields{'bug_id'} && !$fields{'short_desc'}) { 136 $fields{'short_desc'} = $summary; 137 } 138 139 # The Importance/X-Priority headers are only used when creating a new bug. 140 # 1) If somebody specifies a priority, use it. 141 # 2) If there is an Importance or X-Priority header, use it as 142 # something that is relative to the default priority. 143 # If the value is High or 1, increase the priority by 1. 144 # If the value is Low or 5, decrease the priority by 1. 145 # 3) Otherwise, use the default priority. 146 # Note: this will only work if the 'letsubmitterchoosepriority' 147 # parameter is enabled. 148 my $importance = $input_email->header('Importance') 149 || $input_email->header('X-Priority'); 150 if (!$fields{'bug_id'} && !$fields{'priority'} && $importance) { 151 my @legal_priorities = @{get_legal_field_values('priority')}; 152 my $i = firstidx { $_ eq Bugzilla->params->{'defaultpriority'} } @legal_priorities; 153 if ($importance =~ /(high|[12])/i) { 154 $i-- unless $i == 0; 155 } 156 elsif ($importance =~ /(low|[45])/i) { 157 $i++ unless $i == $#legal_priorities; 158 } 159 $fields{'priority'} = $legal_priorities[$i]; 160 } 161 162 my $comment = ''; 163 # Get the description, except the signature. 164 foreach my $line (@body_lines) { 165 last if $line eq SIGNATURE_DELIMITER; 166 $comment .= "$line\n"; 167 } 168 $fields{'comment'} = $comment; 169 170 my %override = %{ $switch{'override'} || {} }; 171 foreach my $key (keys %override) { 172 $fields{$key} = $override{$key}; 173 } 174 175 debug_print("Parsed Fields:\n" . Dumper(\%fields), 2); 176 177 debug_print("Attachments:\n" . Dumper($attachments), 3); 178 if (@$attachments) { 179 $fields{'attachments'} = $attachments; 180 } 181 182 return \%fields; 183} 184 185sub check_email_fields { 186 my ($fields) = @_; 187 188 my ($retval, $non_conclusive_fields) = 189 Bugzilla::User::match_field({ 190 'assigned_to' => { 'type' => 'single' }, 191 'qa_contact' => { 'type' => 'single' }, 192 'cc' => { 'type' => 'multi' }, 193 'newcc' => { 'type' => 'multi' } 194 }, $fields, MATCH_SKIP_CONFIRM); 195 196 if ($retval != USER_MATCH_SUCCESS) { 197 ThrowUserError('user_match_too_many', {fields => $non_conclusive_fields}); 198 } 199} 200 201sub post_bug { 202 my ($fields) = @_; 203 debug_print('Posting a new bug...'); 204 205 my $user = Bugzilla->user; 206 207 check_email_fields($fields); 208 209 my $bug = Bugzilla::Bug->create($fields); 210 debug_print("Created bug " . $bug->id); 211 return ($bug, $bug->comments->[0]); 212} 213 214sub process_bug { 215 my ($fields_in) = @_; 216 my %fields = %$fields_in; 217 218 my $bug_id = $fields{'bug_id'}; 219 $fields{'id'} = $bug_id; 220 delete $fields{'bug_id'}; 221 222 debug_print("Updating Bug $fields{id}..."); 223 224 my $bug = Bugzilla::Bug->check($bug_id); 225 226 if ($fields{'bug_status'}) { 227 $fields{'knob'} = $fields{'bug_status'}; 228 } 229 # If no status is given, then we only want to change the resolution. 230 elsif ($fields{'resolution'}) { 231 $fields{'knob'} = 'change_resolution'; 232 $fields{'resolution_knob_change_resolution'} = $fields{'resolution'}; 233 } 234 if ($fields{'dup_id'}) { 235 $fields{'knob'} = 'duplicate'; 236 } 237 238 # Move @cc to @newcc as @cc is used by process_bug.cgi to remove 239 # users from the CC list when @removecc is set. 240 $fields{'newcc'} = delete $fields{'cc'} if $fields{'cc'}; 241 242 # Make it possible to remove CCs. 243 if ($fields{'removecc'}) { 244 $fields{'cc'} = [split(',', $fields{'removecc'})]; 245 $fields{'removecc'} = 1; 246 } 247 248 check_email_fields(\%fields); 249 250 my $cgi = Bugzilla->cgi; 251 foreach my $field (keys %fields) { 252 $cgi->param(-name => $field, -value => $fields{$field}); 253 } 254 $cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts])); 255 256 require 'process_bug.cgi'; 257 debug_print("Bug processed."); 258 259 my $added_comment; 260 if (trim($fields{'comment'})) { 261 # The "old" bug object doesn't contain the comment we just added. 262 $added_comment = Bugzilla::Bug->check($bug_id)->comments->[-1]; 263 } 264 return ($bug, $added_comment); 265} 266 267sub handle_attachments { 268 my ($bug, $attachments, $comment) = @_; 269 return if !$attachments; 270 debug_print("Handling attachments..."); 271 my $dbh = Bugzilla->dbh; 272 $dbh->bz_start_transaction(); 273 my ($update_comment, $update_bug); 274 foreach my $attachment (@$attachments) { 275 debug_print("Inserting Attachment: " . Dumper($attachment), 3); 276 my $type = $attachment->content_type || 'application/octet-stream'; 277 # MUAs add stuff like "name=" to content-type that we really don't 278 # want. 279 $type =~ s/;.*//; 280 my $obj = Bugzilla::Attachment->create({ 281 bug => $bug, 282 description => $attachment->filename(1), 283 filename => $attachment->filename(1), 284 mimetype => $type, 285 data => $attachment->body, 286 }); 287 # If we added a comment, and our comment does not already have a type, 288 # and this is our first attachment, then we make the comment an 289 # "attachment created" comment. 290 if ($comment and !$comment->type and !$update_comment) { 291 $comment->set_all({ type => CMT_ATTACHMENT_CREATED, 292 extra_data => $obj->id }); 293 $update_comment = 1; 294 } 295 else { 296 $bug->add_comment('', { type => CMT_ATTACHMENT_CREATED, 297 extra_data => $obj->id }); 298 $update_bug = 1; 299 } 300 } 301 # We only update the comments and bugs at the end of the transaction, 302 # because doing so modifies bugs_fulltext, which is a non-transactional 303 # table. 304 $bug->update() if $update_bug; 305 $comment->update() if $update_comment; 306 $dbh->bz_commit_transaction(); 307} 308 309###################### 310# Helper Subroutines # 311###################### 312 313sub debug_print { 314 my ($str, $level) = @_; 315 $level ||= 1; 316 print STDERR "$str\n" if $level <= $switch{'verbose'}; 317} 318 319sub get_body_and_attachments { 320 my ($email) = @_; 321 322 my $ct = $email->content_type || 'text/plain'; 323 debug_print("Splitting Body and Attachments [Type: $ct]...", 2); 324 325 my ($bodies, $attachments) = split_body_and_attachments($email); 326 debug_print(scalar(@$bodies) . " body part(s) and " . scalar(@$attachments) 327 . " attachment part(s)."); 328 debug_print('Bodies: ' . Dumper($bodies), 3); 329 330 # Get the first part of the email that contains a text body, 331 # and make all the other pieces into attachments. (This handles 332 # people or MUAs who accidentally attach text files as an "inline" 333 # attachment.) 334 my $body; 335 while (@$bodies) { 336 my $possible = shift @$bodies; 337 $body = get_text_alternative($possible); 338 if (defined $body) { 339 unshift(@$attachments, @$bodies); 340 last; 341 } 342 } 343 344 if (!defined $body) { 345 # Note that this only happens if the email does not contain any 346 # text/plain parts. If the email has an empty text/plain part, 347 # you're fine, and this message does NOT get thrown. 348 ThrowUserError('email_no_body'); 349 } 350 351 debug_print("Picked Body:\n$body", 2); 352 353 return ($body, $attachments); 354} 355 356sub get_text_alternative { 357 my ($email) = @_; 358 359 my @parts = $email->parts; 360 my $body; 361 foreach my $part (@parts) { 362 my $ct = $part->content_type || 'text/plain'; 363 my $charset = 'iso-8859-1'; 364 # The charset may be quoted. 365 if ($ct =~ /charset="?([^;"]+)/) { 366 $charset= $1; 367 } 368 debug_print("Alternative Part Content-Type: $ct", 2); 369 debug_print("Alternative Part Character Encoding: $charset", 2); 370 # If we find a text/plain body here, return it immediately. 371 if (!$ct || $ct =~ m{^text/plain}i) { 372 return _decode_body($charset, $part->body); 373 } 374 # If we find a text/html body, decode it, but don't return 375 # it immediately, because there might be a text/plain alternative 376 # later. This could be any HTML type. 377 if ($ct =~ m{^application/xhtml\+xml}i or $ct =~ m{text/html}i) { 378 my $parser = HTML::FormatText::WithLinks->new( 379 # Put footnnote indicators after the text, not before it. 380 before_link => '', 381 after_link => '[%n]', 382 # Convert bold and italics, use "*" for bold instead of "_". 383 with_emphasis => 1, 384 bold_marker => '*', 385 # If the same link appears multiple times, only create 386 # one footnote. 387 unique_links => 1, 388 # If the link text is the URL, don't create a footnote. 389 skip_linked_urls => 1, 390 ); 391 $body = _decode_body($charset, $part->body); 392 $body = $parser->parse($body); 393 } 394 } 395 396 return $body; 397} 398 399sub _decode_body { 400 my ($charset, $body) = @_; 401 if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($body)) { 402 return Encode::decode($charset, $body); 403 } 404 return $body; 405} 406 407sub remove_leading_blank_lines { 408 my ($text) = @_; 409 $text =~ s/^(\s*\n)+//s; 410 return $text; 411} 412 413sub html_strip { 414 my ($var) = @_; 415 # Trivial HTML tag remover (this is just for error messages, really.) 416 $var =~ s/<[^>]*>//g; 417 # And this basically reverses the Template-Toolkit html filter. 418 $var =~ s/\&/\&/g; 419 $var =~ s/\</</g; 420 $var =~ s/\>/>/g; 421 $var =~ s/\"/\"/g; 422 $var =~ s/@/@/g; 423 # Also remove undesired newlines and consecutive spaces. 424 $var =~ s/[\n\s]+/ /gms; 425 return $var; 426} 427 428sub split_body_and_attachments { 429 my ($email) = @_; 430 431 my (@body, @attachments); 432 foreach my $part ($email->parts) { 433 my $ct = lc($part->content_type || 'text/plain'); 434 my $disposition = lc($part->header('Content-Disposition') || 'inline'); 435 # Remove the charset, etc. from the content-type, we don't care here. 436 $ct =~ s/;.*//; 437 debug_print("Part Content-Type: [$ct]", 2); 438 debug_print("Part Disposition: [$disposition]", 2); 439 440 if ($disposition eq 'inline' and grep($_ eq $ct, BODY_TYPES)) { 441 push(@body, $part); 442 next; 443 } 444 445 if (scalar($part->parts) == 1) { 446 push(@attachments, $part); 447 next; 448 } 449 450 # If this part has sub-parts, analyze them similarly to how we 451 # did above and return the relevant pieces. 452 my ($add_body, $add_attachments) = split_body_and_attachments($part); 453 push(@body, @$add_body); 454 push(@attachments, @$add_attachments); 455 } 456 457 return (\@body, \@attachments); 458} 459 460 461sub die_handler { 462 my ($msg) = @_; 463 464 # In Template-Toolkit, [% RETURN %] is implemented as a call to "die". 465 # But of course, we really don't want to actually *die* just because 466 # the user-error or code-error template ended. So we don't really die. 467 return if blessed($msg) && $msg->isa('Template::Exception') 468 && $msg->type eq 'return'; 469 470 # If this is inside an eval, then we should just act like...we're 471 # in an eval (instead of printing the error and exiting). 472 die @_ if ($^S // Bugzilla::Error::_in_eval()); 473 474 # We can't depend on the MTA to send an error message, so we have 475 # to generate one properly. 476 if ($input_email) { 477 $msg =~ s/at .+ line.*$//ms; 478 $msg =~ s/^Compilation failed in require.+$//ms; 479 $msg = html_strip($msg); 480 my $from = Bugzilla->params->{'mailfrom'}; 481 my $reply = reply(to => $input_email, from => $from, top_post => 1, 482 body => "$msg\n"); 483 MessageToMTA($reply->as_string); 484 } 485 print STDERR "$msg\n"; 486 # We exit with a successful value, because we don't want the MTA 487 # to *also* send a failure notice. 488 exit; 489} 490 491############### 492# Main Script # 493############### 494 495$SIG{__DIE__} = \&die_handler; 496 497GetOptions(\%switch, 'help|h', 'verbose|v+', 'default=s%', 'override=s%'); 498$switch{'verbose'} ||= 0; 499 500# Print the help message if that switch was selected. 501pod2usage({-verbose => 0, -exitval => 1}) if $switch{'help'}; 502 503Bugzilla->usage_mode(USAGE_MODE_EMAIL); 504 505my @mail_lines = <STDIN>; 506my $mail_text = join("", @mail_lines); 507my $mail_fields = parse_mail($mail_text); 508 509Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields }); 510 511my $attachments = delete $mail_fields->{'attachments'}; 512 513my $username = $mail_fields->{'reporter'}; 514# If emailsuffix is in use, we have to remove it from the email address. 515if (my $suffix = Bugzilla->params->{'emailsuffix'}) { 516 $username =~ s/\Q$suffix\E$//i; 517} 518 519my $user = Bugzilla::User->check($username); 520Bugzilla->set_user($user); 521 522my ($bug, $comment); 523if ($mail_fields->{'bug_id'}) { 524 ($bug, $comment) = process_bug($mail_fields); 525} 526else { 527 ($bug, $comment) = post_bug($mail_fields); 528} 529 530handle_attachments($bug, $attachments, $comment); 531 532# This is here for post_bug and handle_attachments, so that when posting a bug 533# with an attachment, any comment goes out as an attachment comment. 534# 535# Eventually this should be sending the mail for process_bug, too, but we have 536# to wait for $bug->update() to be fully used in email_in.pl first. So 537# currently, process_bug.cgi does the mail sending for bugs, and this does 538# any mail sending for attachments after the first one. 539Bugzilla::BugMail::Send($bug->id, { changer => Bugzilla->user }); 540debug_print("Sent bugmail"); 541 542 543__END__ 544 545=head1 NAME 546 547email_in.pl - The Bugzilla Inbound Email Interface 548 549=head1 SYNOPSIS 550 551./email_in.pl [-vvv] [--default name=value] [--override name=value] < email.txt 552 553Reads an email on STDIN (the standard input). 554 555Options: 556 557 --verbose (-v) - Make the script print more to STDERR. 558 Specify multiple times to print even more. 559 560 --default name=value - Specify defaults for field values, like 561 product=TestProduct. Can be specified multiple 562 times to specify defaults for multiple fields. 563 564 --override name=value - Override field values specified in the email, 565 like product=TestProduct. Can be specified 566 multiple times to override multiple fields. 567 568=head1 DESCRIPTION 569 570This script processes inbound email and creates a bug, or appends data 571to an existing bug. 572 573=head2 Creating a New Bug 574 575The script expects to read an email with the following format: 576 577 From: account@domain.com 578 Subject: Bug Summary 579 580 @product ProductName 581 @component ComponentName 582 @version 1.0 583 584 This is a bug description. It will be entered into the bug exactly as 585 written here. 586 587 It can be multiple paragraphs. 588 589 -- 590 This is a signature line, and will be removed automatically, It will not 591 be included in the bug description. 592 593For the list of valid field names for the C<@> fields, including 594a list of which ones are required, see L<Bugzilla::WebService::Bug/create>. 595(Note, however, that you cannot specify C<@description> as a field-- 596you just add a comment by adding text after the C<@> fields.) 597 598The values for the fields can be split across multiple lines, but 599note that a newline will be parsed as a single space, for the value. 600So, for example: 601 602 @summary This is a very long 603 description 604 605Will be parsed as "This is a very long description". 606 607If you specify C<@summary>, it will override the summary you specify 608in the Subject header. 609 610C<account@domain.com> (the value of the C<From> header) must be a valid 611Bugzilla account. 612 613Note that signatures must start with '-- ', the standard signature 614border. 615 616=head2 Modifying an Existing Bug 617 618Bugzilla determines what bug you want to modify in one of two ways: 619 620=over 621 622=item * 623 624Your subject starts with [Bug 123456] -- then it modifies bug 123456. 625 626=item * 627 628You include C<@id 123456> in the first lines of the email. 629 630=back 631 632If you do both, C<@id> takes precedence. 633 634You send your email in the same format as for creating a bug, except 635that you only specify the fields you want to change. If the very 636first non-blank line of the email doesn't begin with C<@>, then it 637will be assumed that you are only adding a comment to the bug. 638 639Note that when updating a bug, the C<Subject> header is ignored, 640except for getting the bug ID. If you want to change the bug's summary, 641you have to specify C<@summary> as one of the fields to change. 642 643Please remember not to include any extra text in your emails, as that 644text will also be added as a comment. This includes any text that your 645email client automatically quoted and included, if this is a reply to 646another email. 647 648=head3 Adding/Removing CCs 649 650To add CCs, you can specify them in a comma-separated list in C<@cc>. 651 652To remove CCs, specify them as a comma-separated list in C<@removecc>. 653 654=head2 Errors 655 656If your request cannot be completed for any reason, Bugzilla will 657send an email back to you. If your request succeeds, Bugzilla will 658not send you anything. 659 660If any part of your request fails, all of it will fail. No partial 661changes will happen. 662 663=head1 CAUTION 664 665The script does not do any validation that the user is who they say 666they are. That is, it accepts I<any> 'From' address, as long as it's 667a valid Bugzilla account. So make sure that your MTA validates that 668the message is actually coming from who it says it's coming from, 669and only allow access to the inbound email system from people you trust. 670 671=head1 LIMITATIONS 672 673The email interface only accepts emails that are correctly formatted 674per RFC2822. If you send it an incorrectly formatted message, it 675may behave in an unpredictable fashion. 676 677You cannot modify Flags through the email interface. 678