Rubber-stamped by Brady Eidson.
[webbrowser.git] / BugsSite / email_in.pl
blob84cd89635fd97fbeb16cedcc250d1d91cff3a9a5
1 #!/usr/bin/env perl -w
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
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/
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.
14 # The Original Code is the Bugzilla Inbound Email System.
16 # The Initial Developer of the Original Code is Akamai Technologies, Inc.
17 # Portions created by Akamai are Copyright (C) 2006 Akamai Technologies,
18 # Inc. All Rights Reserved.
20 # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
22 use strict;
23 use warnings;
25 # MTAs may call this script from any directory, but it should always
26 # run from this one so that it can find its modules.
27 BEGIN {
28 require File::Basename;
29 chdir(File::Basename::dirname($0));
32 use lib qw(. lib);
34 use Data::Dumper;
35 use Email::Address;
36 use Email::Reply qw(reply);
37 use Email::MIME;
38 use Email::MIME::Attachment::Stripper;
39 use Getopt::Long qw(:config bundling);
40 use Pod::Usage;
41 use Encode;
43 use Bugzilla;
44 use Bugzilla::Bug qw(ValidateBugID);
45 use Bugzilla::Constants qw(USAGE_MODE_EMAIL);
46 use Bugzilla::Error;
47 use Bugzilla::Mailer;
48 use Bugzilla::User;
49 use Bugzilla::Util;
50 use Bugzilla::Token;
52 #############
53 # Constants #
54 #############
56 # This is the USENET standard line for beginning a signature block
57 # in a message. RFC-compliant mailers use this.
58 use constant SIGNATURE_DELIMITER => '-- ';
60 # $input_email is a global so that it can be used in die_handler.
61 our ($input_email, %switch);
63 ####################
64 # Main Subroutines #
65 ####################
67 sub parse_mail {
68 my ($mail_text) = @_;
69 debug_print('Parsing Email');
70 $input_email = Email::MIME->new($mail_text);
72 my %fields;
74 # Email::Address->parse returns an array
75 my ($reporter) = Email::Address->parse($input_email->header('From'));
76 $fields{'reporter'} = $reporter->address;
77 my $summary = $input_email->header('Subject');
78 if ($summary =~ /\[Bug (\d+)\](.*)/i) {
79 $fields{'bug_id'} = $1;
80 $summary = trim($2);
83 my ($body, $attachments) = get_body_and_attachments($input_email);
84 if (@$attachments) {
85 $fields{'attachments'} = $attachments;
88 debug_print("Body:\n" . $body, 3);
90 $body = remove_leading_blank_lines($body);
91 my @body_lines = split(/\r?\n/s, $body);
93 # If there are fields specified.
94 if ($body =~ /^\s*@/s) {
95 my $current_field;
96 while (my $line = shift @body_lines) {
97 # If the sig is starting, we want to keep this in the
98 # @body_lines so that we don't keep the sig as part of the
99 # comment down below.
100 if ($line eq SIGNATURE_DELIMITER) {
101 unshift(@body_lines, $line);
102 last;
104 # Otherwise, we stop parsing fields on the first blank line.
105 $line = trim($line);
106 last if !$line;
108 if ($line =~ /^@(\S+)\s*=\s*(.*)\s*/) {
109 $current_field = lc($1);
110 # It's illegal to pass the reporter field as you could
111 # override the "From:" field of the message and bypass
112 # authentication checks, such as PGP.
113 if ($current_field eq 'reporter') {
114 # We reset the $current_field variable to something
115 # post_bug and process_bug will ignore, in case the
116 # attacker splits the reporter field on several lines.
117 $current_field = 'illegal_field';
118 next;
120 $fields{$current_field} = $2;
122 else {
123 $fields{$current_field} .= " $line";
129 # The summary line only affects us if we're doing a post_bug.
130 # We have to check it down here because there might have been
131 # a bug_id specified in the body of the email.
132 if (!$fields{'bug_id'} && !$fields{'short_desc'}) {
133 $fields{'short_desc'} = $summary;
136 my $comment = '';
137 # Get the description, except the signature.
138 foreach my $line (@body_lines) {
139 last if $line eq SIGNATURE_DELIMITER;
140 $comment .= "$line\n";
142 $fields{'comment'} = $comment;
144 debug_print("Parsed Fields:\n" . Dumper(\%fields), 2);
146 return \%fields;
149 sub post_bug {
150 my ($fields_in) = @_;
151 my %fields = %$fields_in;
153 debug_print('Posting a new bug...');
155 my $cgi = Bugzilla->cgi;
156 foreach my $field (keys %fields) {
157 $cgi->param(-name => $field, -value => $fields{$field});
160 $cgi->param(-name => 'inbound_email', -value => 1);
162 require 'post_bug.cgi';
165 sub process_bug {
166 my ($fields_in) = @_;
168 my %fields = %$fields_in;
170 my $bug_id = $fields{'bug_id'};
171 $fields{'id'} = $bug_id;
172 delete $fields{'bug_id'};
174 debug_print("Updating Bug $fields{id}...");
176 ValidateBugID($bug_id);
177 my $bug = new Bugzilla::Bug($bug_id);
179 if ($fields{'bug_status'}) {
180 $fields{'knob'} = $fields{'bug_status'};
182 # If no status is given, then we only want to change the resolution.
183 elsif ($fields{'resolution'}) {
184 $fields{'knob'} = 'change_resolution';
185 $fields{'resolution_knob_change_resolution'} = $fields{'resolution'};
187 if ($fields{'dup_id'}) {
188 $fields{'knob'} = 'duplicate';
191 # Move @cc to @newcc as @cc is used by process_bug.cgi to remove
192 # users from the CC list when @removecc is set.
193 $fields{'newcc'} = delete $fields{'cc'} if $fields{'cc'};
195 # Make it possible to remove CCs.
196 if ($fields{'removecc'}) {
197 $fields{'cc'} = [split(',', $fields{'removecc'})];
198 $fields{'removecc'} = 1;
201 my $cgi = Bugzilla->cgi;
202 foreach my $field (keys %fields) {
203 $cgi->param(-name => $field, -value => $fields{$field});
205 $cgi->param('longdesclength', scalar $bug->longdescs);
206 $cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts]));
208 require 'process_bug.cgi';
211 ######################
212 # Helper Subroutines #
213 ######################
215 sub debug_print {
216 my ($str, $level) = @_;
217 $level ||= 1;
218 print STDERR "$str\n" if $level <= $switch{'verbose'};
221 sub get_body_and_attachments {
222 my ($email) = @_;
224 my $ct = $email->content_type || 'text/plain';
225 debug_print("Splitting Body and Attachments [Type: $ct]...");
227 my $body;
228 my $attachments = [];
229 if ($ct =~ /^multipart\/alternative/i) {
230 $body = get_text_alternative($email);
232 else {
233 my $stripper = new Email::MIME::Attachment::Stripper(
234 $email, force_filename => 1);
235 my $message = $stripper->message;
236 $body = get_text_alternative($message);
237 $attachments = [$stripper->attachments];
240 return ($body, $attachments);
243 sub get_text_alternative {
244 my ($email) = @_;
246 my @parts = $email->parts;
247 my $body;
248 foreach my $part (@parts) {
249 my $ct = $part->content_type || 'text/plain';
250 my $charset = 'iso-8859-1';
251 # The charset may be quoted.
252 if ($ct =~ /charset="?([^;"]+)/) {
253 $charset= $1;
255 debug_print("Part Content-Type: $ct", 2);
256 debug_print("Part Character Encoding: $charset", 2);
257 if (!$ct || $ct =~ /^text\/plain/i) {
258 $body = $part->body;
259 if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($body)) {
260 $body = Encode::decode($charset, $body);
262 last;
266 if (!defined $body) {
267 # Note that this only happens if the email does not contain any
268 # text/plain parts. If the email has an empty text/plain part,
269 # you're fine, and this message does NOT get thrown.
270 ThrowUserError('email_no_text_plain');
273 return $body;
276 sub remove_leading_blank_lines {
277 my ($text) = @_;
278 $text =~ s/^(\s*\n)+//s;
279 return $text;
282 sub html_strip {
283 my ($var) = @_;
284 # Trivial HTML tag remover (this is just for error messages, really.)
285 $var =~ s/<[^>]*>//g;
286 # And this basically reverses the Template-Toolkit html filter.
287 $var =~ s/\&amp;/\&/g;
288 $var =~ s/\&lt;/</g;
289 $var =~ s/\&gt;/>/g;
290 $var =~ s/\&quot;/\"/g;
291 $var =~ s/&#64;/@/g;
292 # Also remove undesired newlines and consecutive spaces.
293 $var =~ s/[\n\s]+/ /gms;
294 return $var;
298 sub die_handler {
299 my ($msg) = @_;
301 # In Template-Toolkit, [% RETURN %] is implemented as a call to "die".
302 # But of course, we really don't want to actually *die* just because
303 # the user-error or code-error template ended. So we don't really die.
304 return if $msg->isa('Template::Exception') && $msg->type eq 'return';
306 # If this is inside an eval, then we should just act like...we're
307 # in an eval (instead of printing the error and exiting).
308 die(@_) if $^S;
310 # We can't depend on the MTA to send an error message, so we have
311 # to generate one properly.
312 if ($input_email) {
313 $msg =~ s/at .+ line.*$//ms;
314 $msg =~ s/^Compilation failed in require.+$//ms;
315 $msg = html_strip($msg);
316 my $from = Bugzilla->params->{'mailfrom'};
317 my $reply = reply(to => $input_email, from => $from, top_post => 1,
318 body => "$msg\n");
319 MessageToMTA($reply->as_string);
321 print STDERR "$msg\n";
322 # We exit with a successful value, because we don't want the MTA
323 # to *also* send a failure notice.
324 exit;
327 ###############
328 # Main Script #
329 ###############
331 $SIG{__DIE__} = \&die_handler;
333 GetOptions(\%switch, 'help|h', 'verbose|v+');
334 $switch{'verbose'} ||= 0;
336 # Print the help message if that switch was selected.
337 pod2usage({-verbose => 0, -exitval => 1}) if $switch{'help'};
339 Bugzilla->usage_mode(USAGE_MODE_EMAIL);
342 my @mail_lines = <STDIN>;
343 my $mail_text = join("", @mail_lines);
344 my $mail_fields = parse_mail($mail_text);
346 my $username = $mail_fields->{'reporter'};
347 # If emailsuffix is in use, we have to remove it from the email address.
348 if (my $suffix = Bugzilla->params->{'emailsuffix'}) {
349 $username =~ s/\Q$suffix\E$//i;
352 my $user = Bugzilla::User->new({ name => $username })
353 || ThrowUserError('invalid_username', { name => $username });
355 Bugzilla->set_user($user);
357 if ($mail_fields->{'bug_id'}) {
358 process_bug($mail_fields);
360 else {
361 post_bug($mail_fields);
364 __END__
366 =head1 NAME
368 email_in.pl - The Bugzilla Inbound Email Interface
370 =head1 SYNOPSIS
372 ./email_in.pl [-vvv] < email.txt
374 Reads an email on STDIN (the standard input).
376 Options:
377 --verbose (-v) - Make the script print more to STDERR.
378 Specify multiple times to print even more.
380 =head1 DESCRIPTION
382 This script processes inbound email and creates a bug, or appends data
383 to an existing bug.
385 =head2 Creating a New Bug
387 The script expects to read an email with the following format:
389 From: account@domain.com
390 Subject: Bug Summary
392 @product = ProductName
393 @component = ComponentName
394 @version = 1.0
396 This is a bug description. It will be entered into the bug exactly as
397 written here.
399 It can be multiple paragraphs.
402 This is a signature line, and will be removed automatically, It will not
403 be included in the bug description.
405 The C<@> labels can be any valid field name in Bugzilla that can be
406 set on C<enter_bug.cgi>. For the list of required field names, see
407 L<Bugzilla::WebService::Bug/Create>. Note, that there is some difference
408 in the names of the required input fields between web and email interfaces,
409 as listed below:
411 =over
413 =item *
415 C<platform> in web is C<@rep_platform> in email
417 =item *
419 C<severity> in web is C<@bug_severity> in email
421 =back
423 For the list of all field names, see the C<fielddefs> table in the database.
425 The values for the fields can be split across multiple lines, but
426 note that a newline will be parsed as a single space, for the value.
427 So, for example:
429 @short_desc = This is a very long
430 description
432 Will be parsed as "This is a very long description".
434 If you specify C<@short_desc>, it will override the summary you specify
435 in the Subject header.
437 C<account@domain.com> must be a valid Bugzilla account.
439 Note that signatures must start with '-- ', the standard signature
440 border.
442 =head2 Modifying an Existing Bug
444 Bugzilla determines what bug you want to modify in one of two ways:
446 =over
448 =item *
450 Your subject starts with [Bug 123456] -- then it modifies bug 123456.
452 =item *
454 You include C<@bug_id = 123456> in the first lines of the email.
456 =back
458 If you do both, C<@bug_id> takes precedence.
460 You send your email in the same format as for creating a bug, except
461 that you only specify the fields you want to change. If the very
462 first non-blank line of the email doesn't begin with C<@>, then it
463 will be assumed that you are only adding a comment to the bug.
465 Note that when updating a bug, the C<Subject> header is ignored,
466 except for getting the bug ID. If you want to change the bug's summary,
467 you have to specify C<@short_desc> as one of the fields to change.
469 Please remember not to include any extra text in your emails, as that
470 text will also be added as a comment. This includes any text that your
471 email client automatically quoted and included, if this is a reply to
472 another email.
474 =head3 Adding/Removing CCs
476 To add CCs, you can specify them in a comma-separated list in C<@cc>.
477 For backward compatibility, C<@newcc> can also be used. If both are
478 present, C<@cc> takes precedence.
480 To remove CCs, specify them as a comma-separated list in C<@removecc>.
482 =head2 Errors
484 If your request cannot be completed for any reason, Bugzilla will
485 send an email back to you. If your request succeeds, Bugzilla will
486 not send you anything.
488 If any part of your request fails, all of it will fail. No partial
489 changes will happen.
491 There is no attachment support yet.
493 =head1 CAUTION
495 The script does not do any validation that the user is who they say
496 they are. That is, it accepts I<any> 'From' address, as long as it's
497 a valid Bugzilla account. So make sure that your MTA validates that
498 the message is actually coming from who it says it's coming from,
499 and only allow access to the inbound email system from people you trust.
501 =head1 LIMITATIONS
503 Note that the email interface has the same limitations as the
504 normal Bugzilla interface. So, for example, you cannot reassign
505 a bug and change its status at the same time.
507 The email interface only accepts emails that are correctly formatted
508 perl RFC2822. If you send it an incorrectly formatted message, it
509 may behave in an unpredictable fashion.
511 You cannot send an HTML mail along with attachments. If you do, Bugzilla
512 will reject your email, saying that it doesn't contain any text. This
513 is a bug in L<Email::MIME::Attachment::Stripper> that we can't work
514 around.
516 You cannot modify Flags through the email interface.