From e395f89f73b10b773192b4fe4c2726eeee7e05bb Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Thu, 28 Dec 2017 00:22:33 +0300 Subject: [PATCH] HTTP incoming email handler --- Bugzilla/Config/MTA.pm | 6 + Bugzilla/InMail.pm | 462 +++++++++++++++++ email_in.cgi | 73 +++ email_in.pl | 489 +----------------- .../en/default/admin/params/mta.html.tmpl | 8 + 5 files changed, 556 insertions(+), 482 deletions(-) create mode 100644 Bugzilla/InMail.pm create mode 100755 email_in.cgi diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm index a4d3d6857..c685a5f4a 100644 --- a/Bugzilla/Config/MTA.pm +++ b/Bugzilla/Config/MTA.pm @@ -103,6 +103,12 @@ sub get_param_list default => 1 }, + { + name => 'enable_inmail_cgi', + type => 'b', + default => 0 + }, + { name => 'smtpserver', type => 't', diff --git a/Bugzilla/InMail.pm b/Bugzilla/InMail.pm new file mode 100644 index 000000000..988a585a7 --- /dev/null +++ b/Bugzilla/InMail.pm @@ -0,0 +1,462 @@ +# Incoming mail handler for Bugzilla +# License: Dual-license GPL 3.0+ or MPL 1.1+ +# Contributor(s): Vitaliy Filippov , Max Kanat-Alexander + +package Bugzilla::InMail; + +use strict; +use warnings; + +use Email::Address; +use Email::Reply qw(reply); +use Email::MIME; +use Email::MIME::Attachment::Stripper; +use HTML::Strip; +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::Hook; +use Bugzilla::BugMail; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Mailer; +use Bugzilla::Token; +use Bugzilla::User; +use Bugzilla::Util; + +############# +# Constants # +############# + +# This is the USENET standard line for beginning a signature block +# in a message. RFC-compliant mailers use this. +use constant SIGNATURE_DELIMITER => '-- '; + +sub process_inmail +{ + my ($mail_text) = @_; + + my $input_email = Email::MIME->new($mail_text); + + my $status = eval + { + my $mail_fields = parse_mail($input_email); + if (!$mail_fields) + { + return 0; + } + + Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields }); + + my $attachments = delete $mail_fields->{attachments}; + select_user($mail_fields->{reporter}, $mail_fields->{_reporter_name}); + + my ($bug, $comment); + if ($mail_fields->{bug_id}) + { + $bug = Bugzilla::Bug::create_or_update($mail_fields); + $comment = $bug->comments->[-1] if trim($mail_fields->{comment}); + } + else + { + ($bug, $comment) = post_bug($mail_fields); + } + + handle_attachments($bug, $attachments, $comment); + + Bugzilla->send_mail; + + return 1; + }; + + if ($@) + { + # Report error to the sender of original message + my $msg = $@; + if (ref $msg eq 'Bugzilla::Error') + { + $msg = $msg->{message}; + } + if ($input_email) + { + my $from = Bugzilla->params->{mailfrom}; + my $reply = reply(to => $input_email, from => $from, top_post => 1, body => "$msg\n"); + MessageToMTA($reply->as_string); + } + return -1; + } + + return $status; +} + +sub select_user +{ + my ($reporter, $reporter_name) = @_; + + my $username = $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; + } + + # First try to select user with name $username + my $user = Bugzilla::User->new({ name => $username }); + + # Then try to find alias $username for some user + unless ($user) + { + my $dbh = Bugzilla->dbh; + ($user) = $dbh->selectrow_array("SELECT userid FROM emailin_aliases WHERE address=?", undef, trim($reporter)); + $user = Bugzilla::User->new({ id => $user }) if $user; + # Then check if autoregistration is enabled + unless ($user) + { + unless (Bugzilla->params->{emailin_autoregister}) + { + ThrowUserError('invalid_username', { name => $username }); + } + # Then try to autoregister unknown user + $user = Bugzilla::User->create({ + login_name => $username, + realname => $reporter_name, + cryptpassword => 'a3#', + disabledtext => '', + }); + } + } + + if (!$user->is_enabled) + { + ThrowUserError('account_disabled', { disabled_reason => $user->disabledtext }); + } + + Bugzilla->set_user($user); +} + +sub parse_mail +{ + my ($input_email) = @_; + + my %fields; + Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, fields => \%fields }); + # RFC 3834 - Recommendations for Automatic Responses to Electronic Mail + # Automatic responses SHOULD NOT be issued in response to any + # message which contains an Auto-Submitted header field (see below), + # where that field has any value other than "no". + # F*cking MS Exchange sometimes does not append Auto-Submitted header + # to delivery status reports, so also check content-type. + my $autosubmitted; + if (lc($input_email->header('Auto-Submitted') || 'no') ne 'no' || + ($input_email->header('X-Auto-Response-Suppress') || '') =~ /all/iso || + ($input_email->header('Content-Type') || '') =~ /delivery-status/iso) + { + return undef; + } + + my $dbh = Bugzilla->dbh; + + # Fetch field => value from emailin_fields table + my ($toemail) = Email::Address->parse($input_email->header('To')); + %fields = (%fields, map { @$_ } @{ $dbh->selectall_arrayref( + "SELECT field, value FROM emailin_fields WHERE address=?", + undef, $toemail) || [] }); + + my $summary = $input_email->header('Subject'); + if ($summary =~ /\[\s*Bug\s*(\d+)\s*\](.*)/i) + { + $fields{bug_id} = $1; + $summary = trim($2); + } + $fields{_subject} = $summary; + + # Add CC's from email Cc: header + $fields{newcc} = $input_email->header('Cc'); + $fields{newcc} = $fields{newcc} && (join ', ', map { [ Email::Address->parse($_) ] -> [0] } + split /\s*,\s*/, $fields{newcc}) || undef; + + my ($body, $attachments) = get_body_and_attachments($input_email); + if (@$attachments) + { + $fields{attachments} = $attachments; + } + + $body = remove_leading_blank_lines($body); + + Bugzilla::Hook::process("emailin-filter_body", { body => \$body }); + + my @body_lines = split(/\r?\n/s, $body); + my $fields_by_name = { map { (lc($_->description) => $_->name, lc($_->name) => $_->name) } Bugzilla->get_fields({ obsolete => 0 }) }; + + # If there are fields specified. + if ($body =~ /^\s*@/s) + { + my $current_field; + while (my $line = shift @body_lines) + { + # If the sig is starting, we want to keep this in the + # @body_lines so that we don't keep the sig as part of the + # comment down below. + if ($line eq SIGNATURE_DELIMITER) + { + unshift(@body_lines, $line); + last; + } + # Otherwise, we stop parsing fields on the first blank line. + $line = trim($line); + last if !$line; + if ($line =~ /^\@\s*(.+?)\s*=\s*(.*)\s*/) + { + $current_field = $fields_by_name->{lc($1)} || lc($1); + $fields{$current_field} = $2; + } + else + { + $fields{$current_field} .= " $line"; + } + } + } + + %fields = %{ Bugzilla::Bug::map_fields(\%fields) }; + + my ($reporter) = Email::Address->parse($input_email->header('From')); + $fields{reporter} = $reporter->address; + + { + my $r; + if ($r = $reporter->phrase) + { + $r .= ' ' . $reporter->comment if $reporter->comment; + } + else + { + $r = $reporter->address; + } + $fields{_reporter_name} = $r; + } + + # The summary line only affects us if we're doing a post_bug. + # We have to check it down here because there might have been + # a bug_id specified in the body of the email. + if (!$fields{bug_id} && !$fields{short_desc}) + { + $fields{short_desc} = $summary; + } + + my $comment = ''; + # Get the description, except the signature. + foreach my $line (@body_lines) + { + last if $line eq SIGNATURE_DELIMITER; + $comment .= "$line\n"; + } + $fields{comment} = $comment; + + return \%fields; +} + +sub post_bug +{ + my ($fields) = @_; + my $bug; + $Bugzilla::Error::IN_EVAL++; + eval + { + 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 }); + } + $bug = Bugzilla::Bug::create_or_update($fields); + }; + $Bugzilla::Error::IN_EVAL--; + if (my $err = $@) + { + my $format = "\n\nIncoming mail format for entering bugs:\n\n\@field = value\n\@field = value\n...\n\n\n"; + if (blessed $err && $err->{message}) + { + $err->{message} .= $format; + } + else + { + $err .= $format; + } + die $err; + } + if ($bug) + { + return ($bug, $bug->comments->[0]); + } + return undef; +} + +sub handle_attachments +{ + my ($bug, $attachments, $comment) = @_; + return if !$attachments; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my ($update_comment, $update_bug); + foreach my $attachment (@$attachments) + { + my $data = delete $attachment->{payload}; + $attachment->{content_type} ||= 'application/octet-stream'; + my $obj = Bugzilla::Attachment->create({ + bug => $bug, + description => $attachment->{filename}, + filename => $attachment->{filename}, + mimetype => $attachment->{content_type}, + 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_type(CMT_ATTACHMENT_CREATED, $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 get_body_and_attachments +{ + my ($email) = @_; + + my $ct = $email->content_type || 'text/plain'; + + my $body; + my $attachments = []; + 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); + $attachments = [$stripper->attachments]; + } + $email->charset_set('utf8'); + $email->body_str_set($body); + + return ($body, $attachments); +} + +sub rm_line_feeds +{ + my ($t) = @_; + $t =~ s/[\n\r]+/ /giso; + return $t; +} + +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; + } + if (!$ct || $ct =~ /^text\/plain/i) + { + $body = $part->body; + } + elsif ($ct =~ /^text\/html/i) + { + $body = $part->body; + $body =~ s/]*class=[\"\']?difft[^<>]*>.*?<\/table\s*>//giso; + $body =~ s/(]*>.*?<\/a\s*>)/rm_line_feeds($1)/gieso; + Bugzilla::Hook::process("emailin-filter_html", { body => \$body }); + $body = HTML::Strip->new->parse($body); + } + if (defined $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 remove_leading_blank_lines +{ + my ($text) = @_; + $text =~ s/^(\s*\n)+//s; + return $text; +} + +# Use UTF-8 in Email::Reply to correctly quote the body +my $crlf = "\x0d\x0a"; +my $CRLF = $crlf; +undef *Email::Reply::_quote_body; +*Email::Reply::_quote_body = sub +{ + my ($self, $part) = @_; + return if length $self->{quoted}; + return map $self->_quote_body($_), $part->parts if $part->parts > 1; + return if $part->content_type && $part->content_type !~ m[\btext/plain\b]; + + my $body = $part->body; + Encode::_utf8_on($body); + + $body = ($self->_strip_sig($body) || $body) + if !$self->{keep_sig} && $body =~ /$crlf--\s*$crlf/o; + + my ($end) = $body =~ /($crlf)/; + $end ||= $CRLF; + $body =~ s/[\r\n\s]+$//; + $body = $self->_quote_orig_body($body); + $body = "$self->{attrib}$end$body$end"; + + $self->{crlf} = $end; + $self->{quoted} = $body; +}; + +1; +__END__ diff --git a/email_in.cgi b/email_in.cgi new file mode 100755 index 000000000..5156fc667 --- /dev/null +++ b/email_in.cgi @@ -0,0 +1,73 @@ +#!/usr/bin/perl -wT +# HTTP handler for incoming e-mail + +use strict; +use lib qw(. lib); + +use Bugzilla; +use Bugzilla::InMail; + +my $status; +if (!Bugzilla->params->{enable_inmail_cgi}) +{ + $status = 'disabled'; +} +else +{ + my $mail_text = Bugzilla->cgi->param('POSTDATA'); + if (!$mail_text) + { + $status = 'empty-message'; + } + else + { + $status = Bugzilla::InMail::process_inmail($mail_text) == 1 ? 'success' : 'error'; + } +} + +Bugzilla->cgi->send_header('application/json'); +print '{"status":"'.$status.'"}'; +exit; + +__END__ + +Postfix configuration example: + +1) If you want to log all incoming messages, create /etc/postfix/send-to-bugzilla script with the following content: + +#!/bin/sh +echo '-----' >> /var/log/bugzilla-email-in.log +/usr/bin/tee -a /var/log/bugzilla-email-in.log | curl -X POST -H 'Content-Type: text/plain' --data-binary @- http://127.0.0.1:8157/email_in.cgi + +2) If you don't want to log all incoming messages, create /etc/postfix/send-to-bugzilla script with the following content: + +#!/bin/sh +curl -X POST -H 'Content-Type: text/plain' --data-binary @- http://127.0.0.1:8157/email_in.cgi + +3) Make it executable: + +chmod 755 /etc/postfix/send-to-bugzilla + +4) Add the following to master.cf: + +bugzilla unix - n n - - pipe + flags=DRhu user=www-data:www-data argv=/etc/postfix/send-to-bugzilla + +5) Add the following to your /etc/postfix/transport map: + +daemon@your.bugzilla.url bugzilla: + +Where `daemon@your.bugzilla.url` is the same as `mailfrom` Bugzilla parameter from Administration -> Config +This will make your Postfix feed all messages sent to `daemon@your.bugzilla.url` to email_in.cgi. + +6) Run `postmap /etc/postfix/transport` + +7) Ensure that other parts of your Postfix configuration do not prevent it from receiving mail to daemon@your.bugzilla.url + +8) Turn `enable_inmail_cgi` parameter on in Administration -> Config + +9) Deny access to `email_in.cgi` in your HTTP server. For example with nginx: + +location /email_in.cgi { + deny all; +} diff --git a/email_in.pl b/email_in.pl index 2e98e8152..c70f7d168 100755 --- a/email_in.pl +++ b/email_in.pl @@ -30,430 +30,16 @@ BEGIN my ($a) = abs_path($0) =~ /^(.*)$/; chdir dirname($a); } - use lib qw(. lib); +use Bugzilla::InMail; -use Data::Dumper; -use Email::Address; -use Email::Reply qw(reply); -use Email::MIME; -use Email::MIME::Attachment::Stripper; -use HTML::Strip; -use Getopt::Long qw(:config bundling); -use Pod::Usage; -use Encode; -use Scalar::Util qw(blessed); +my $switch = {}; -use Bugzilla; -use Bugzilla::Attachment; -use Bugzilla::Bug; -use Bugzilla::Hook; -use Bugzilla::BugMail; -use Bugzilla::Constants; -use Bugzilla::Error; -use Bugzilla::Mailer; -use Bugzilla::Token; -use Bugzilla::User; -use Bugzilla::Util; - -############# -# Constants # -############# - -# This is the USENET standard line for beginning a signature block -# in a message. RFC-compliant mailers use this. -use constant SIGNATURE_DELIMITER => '-- '; - -# $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; - Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, fields => \%fields }); - # RFC 3834 - Recommendations for Automatic Responses to Electronic Mail - # Automatic responses SHOULD NOT be issued in response to any - # message which contains an Auto-Submitted header field (see below), - # where that field has any value other than "no". - # F*cking MS Exchange sometimes does not append Auto-Submitted header - # to delivery status reports, so also check content-type. - my $autosubmitted; - if (lc($input_email->header('Auto-Submitted') || 'no') ne 'no' || - ($input_email->header('X-Auto-Response-Suppress') || '') =~ /all/iso || - ($input_email->header('Content-Type') || '') =~ /delivery-status/iso) - { - debug_print("Rejecting email with Auto-Submitted = $autosubmitted"); - exit 0; - } - - my $dbh = Bugzilla->dbh; - - # Fetch field => value from emailin_fields table - my ($toemail) = Email::Address->parse($input_email->header('To')); - %fields = (%fields, map { @$_ } @{ $dbh->selectall_arrayref( - "SELECT field, value FROM emailin_fields WHERE address=?", - undef, $toemail) || [] }); - - my $summary = $input_email->header('Subject'); - if ($summary =~ /\[\s*Bug\s*(\d+)\s*\](.*)/i) - { - $fields{bug_id} = $1; - $summary = trim($2); - } - $fields{_subject} = $summary; - - # Add CC's from email Cc: header - $fields{newcc} = $input_email->header('Cc'); - $fields{newcc} = $fields{newcc} && (join ', ', map { [ Email::Address->parse($_) ] -> [0] } - split /\s*,\s*/, $fields{newcc}) || undef; - - my ($body, $attachments) = get_body_and_attachments($input_email); - if (@$attachments) - { - $fields{attachments} = $attachments; - } - - debug_print("Body:\n" . $body, 3); - - $body = remove_leading_blank_lines($body); - - Bugzilla::Hook::process("emailin-filter_body", { body => \$body }); - - my @body_lines = split(/\r?\n/s, $body); - my $fields_by_name = { map { (lc($_->description) => $_->name, lc($_->name) => $_->name) } Bugzilla->get_fields({ obsolete => 0 }) }; - - # If there are fields specified. - if ($body =~ /^\s*@/s) - { - my $current_field; - while (my $line = shift @body_lines) - { - # If the sig is starting, we want to keep this in the - # @body_lines so that we don't keep the sig as part of the - # comment down below. - if ($line eq SIGNATURE_DELIMITER) - { - unshift(@body_lines, $line); - last; - } - # Otherwise, we stop parsing fields on the first blank line. - $line = trim($line); - last if !$line; - if ($line =~ /^\@\s*(.+?)\s*=\s*(.*)\s*/) - { - $current_field = $fields_by_name->{lc($1)} || lc($1); - $fields{$current_field} = $2; - } - else - { - $fields{$current_field} .= " $line"; - } - } - } - - %fields = %{ Bugzilla::Bug::map_fields(\%fields) }; - - my ($reporter) = Email::Address->parse($input_email->header('From')); - $fields{reporter} = $reporter->address; - - { - my $r; - if ($r = $reporter->phrase) - { - $r .= ' ' . $reporter->comment if $reporter->comment; - } - else - { - $r = $reporter->address; - } - $fields{_reporter_name} = $r; - } - - # The summary line only affects us if we're doing a post_bug. - # We have to check it down here because there might have been - # a bug_id specified in the body of the email. - if (!$fields{bug_id} && !$fields{short_desc}) - { - $fields{short_desc} = $summary; - } - - my $comment = ''; - # Get the description, except the signature. - foreach my $line (@body_lines) - { - last if $line eq SIGNATURE_DELIMITER; - $comment .= "$line\n"; - } - $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 $bug; - $Bugzilla::Error::IN_EVAL++; - eval - { - 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 }); - } - $bug = Bugzilla::Bug::create_or_update($fields); - }; - $Bugzilla::Error::IN_EVAL--; - if (my $err = $@) - { - my $format = "\n\nIncoming mail format for entering bugs:\n\n\@field = value\n\@field = value\n...\n\n\n"; - if (blessed $err && $err->{message}) - { - $err->{message} .= $format; - } - else - { - $err .= $format; - } - die $err; - } - if ($bug) - { - debug_print("Created bug " . $bug->id); - return ($bug, $bug->comments->[0]); - } - return undef; -} - -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 $obj = Bugzilla::Attachment->create({ - bug => $bug, - description => $attachment->{filename}, - filename => $attachment->{filename}, - mimetype => $attachment->{content_type}, - 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_type(CMT_ATTACHMENT_CREATED, $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_and_attachments -{ - my ($email) = @_; - - my $ct = $email->content_type || 'text/plain'; - debug_print("Splitting Body and Attachments [Type: $ct]..."); - - my $body; - my $attachments = []; - 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); - $attachments = [$stripper->attachments]; - } - $email->charset_set('utf8'); - $email->body_str_set($body); - - return ($body, $attachments); -} - -sub rm_line_feeds -{ - my ($t) = @_; - $t =~ s/[\n\r]+/ /giso; - return $t; -} - -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; - } - elsif ($ct =~ /^text\/html/i) - { - $body = $part->body; - $body =~ s/]*class=[\"\']?difft[^<>]*>.*?<\/table\s*>//giso; - $body =~ s/(]*>.*?<\/a\s*>)/rm_line_feeds($1)/gieso; - Bugzilla::Hook::process("emailin-filter_html", { body => \$body }); - $body = HTML::Strip->new->parse($body); - } - if (defined $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 remove_leading_blank_lines -{ - my ($text) = @_; - $text =~ s/^(\s*\n)+//s; - return $text; -} - -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; - - if (ref $msg eq 'Bugzilla::Error') - { - $msg = $msg->{message}; - } - - # We can't depend on the MTA to send an error message, so we have - # to generate one properly. - if ($input_email) - { - 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; -} - -# Use UTF-8 in Email::Reply to correctly quote the body -my $crlf = "\x0d\x0a"; -my $CRLF = $crlf; -undef *Email::Reply::_quote_body; -*Email::Reply::_quote_body = sub -{ - my ($self, $part) = @_; - return if length $self->{quoted}; - return map $self->_quote_body($_), $part->parts if $part->parts > 1; - return if $part->content_type && $part->content_type !~ m[\btext/plain\b]; - - my $body = $part->body; - Encode::_utf8_on($body); - - $body = ($self->_strip_sig($body) || $body) - if !$self->{keep_sig} && $body =~ /$crlf--\s*$crlf/o; - - my ($end) = $body =~ /($crlf)/; - $end ||= $CRLF; - $body =~ s/[\r\n\s]+$//; - $body = $self->_quote_orig_body($body); - $body = "$self->{attrib}$end$body$end"; - - $self->{crlf} = $end; - $self->{quoted} = $body; -}; - -############### -# Main Script # -############### - -$SIG{__DIE__} = \&die_handler; - -GetOptions(\%switch, 'help|h', 'verbose|v+'); -$switch{verbose} ||= 0; +GetOptions($switch, 'help|h', 'verbose|v+'); +$switch->{verbose} ||= 0; # Print the help message if that switch was selected. -pod2usage({-verbose => 0, -exitval => 1}) if $switch{help}; +pod2usage({-verbose => 0, -exitval => 1}) if $switch->{help}; # Get a next-in-pipe command from commandline my ($pipe) = join(' ', @ARGV) =~ /^(.*)$/iso; @@ -472,69 +58,8 @@ if ($pipe && open PIPE, "| $pipe") close PIPE; } -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; -} - -# First try to select user with name $username -my $user = Bugzilla::User->new({ name => $username }); - -# Then try to find alias $username for some user -unless ($user) -{ - my $dbh = Bugzilla->dbh; - ($user) = $dbh->selectrow_array("SELECT userid FROM emailin_aliases WHERE address=?", undef, trim($mail_fields->{reporter})); - $user = Bugzilla::User->new({ id => $user }) if $user; - # Then check if autoregistration is enabled - unless ($user) - { - unless (Bugzilla->params->{emailin_autoregister}) - { - ThrowUserError('invalid_username', { name => $username }); - } - # Then try to autoregister unknown user - $user = Bugzilla::User->create({ - login_name => $username, - realname => $mail_fields->{_reporter_name}, - cryptpassword => 'a3#', - disabledtext => '', - }); - } -} - -if (!$user->is_enabled) -{ - ThrowUserError('account_disabled', { disabled_reason => $user->disabledtext }); -} - -Bugzilla->set_user($user); - -my ($bug, $comment); -if ($mail_fields->{bug_id}) -{ - debug_print("Updating Bug $mail_fields->{bug_id}..."); - $bug = Bugzilla::Bug::create_or_update($mail_fields); - $comment = $bug->comments->[-1] if trim($mail_fields->{comment}); -} -else -{ - ($bug, $comment) = post_bug($mail_fields); -} - -handle_attachments($bug, $attachments, $comment); - -Bugzilla->send_mail; - -debug_print("Sent bugmail"); +Bugzilla::InMail::process_inmail($mail_text); +exit; __END__ diff --git a/template/en/default/admin/params/mta.html.tmpl b/template/en/default/admin/params/mta.html.tmpl index e7a443e6c..1f885a842 100644 --- a/template/en/default/admin/params/mta.html.tmpl +++ b/template/en/default/admin/params/mta.html.tmpl @@ -56,6 +56,14 @@ _ " won't get sent). This affects all mail sent by $terms.Bugzilla," _ " not just $terms.bug updates.", + enable_inmail_cgi => + "Enable HTTP handler for incoming e-mail (email_in.cgi). " _ + "IMPORTANT NOTE: This handler is only for usage from your MTA. " _ + "If you enable it, you MUST make sure that your nginx (or other http " _ + "reverse proxy Bugzilla4Intranet is installed behind) denies access to " _ + "email_in.cgi from public addresses. " _ + "See the end of email_in.cgi for an example Postfix configuration.", + sendmailnow => "Sites using anything older than version 8.12 of 'sendmail' " _ "can achieve a significant performance increase in the " _ "UI -- at the cost of delaying the sending of mail -- by " _