HTTP incoming email handler
parent
0122af7a78
commit
e395f89f73
|
@ -103,6 +103,12 @@ sub get_param_list
|
||||||
default => 1
|
default => 1
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name => 'enable_inmail_cgi',
|
||||||
|
type => 'b',
|
||||||
|
default => 0
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name => 'smtpserver',
|
name => 'smtpserver',
|
||||||
type => 't',
|
type => 't',
|
||||||
|
|
|
@ -0,0 +1,462 @@
|
||||||
|
# Incoming mail handler for Bugzilla
|
||||||
|
# License: Dual-license GPL 3.0+ or MPL 1.1+
|
||||||
|
# Contributor(s): Vitaliy Filippov <vitalif@mail.ru>, Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||||
|
|
||||||
|
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<Bug description...>\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/<table[^<>]*class=[\"\']?difft[^<>]*>.*?<\/table\s*>//giso;
|
||||||
|
$body =~ s/(<a[^<>]*>.*?<\/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__
|
|
@ -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;
|
||||||
|
}
|
489
email_in.pl
489
email_in.pl
|
@ -30,430 +30,16 @@ BEGIN
|
||||||
my ($a) = abs_path($0) =~ /^(.*)$/;
|
my ($a) = abs_path($0) =~ /^(.*)$/;
|
||||||
chdir dirname($a);
|
chdir dirname($a);
|
||||||
}
|
}
|
||||||
|
|
||||||
use lib qw(. lib);
|
use lib qw(. lib);
|
||||||
|
use Bugzilla::InMail;
|
||||||
|
|
||||||
use Data::Dumper;
|
my $switch = {};
|
||||||
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;
|
GetOptions($switch, 'help|h', 'verbose|v+');
|
||||||
use Bugzilla::Attachment;
|
$switch->{verbose} ||= 0;
|
||||||
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<Bug description...>\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/<table[^<>]*class=[\"\']?difft[^<>]*>.*?<\/table\s*>//giso;
|
|
||||||
$body =~ s/(<a[^<>]*>.*?<\/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;
|
|
||||||
|
|
||||||
# Print the help message if that switch was selected.
|
# 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
|
# Get a next-in-pipe command from commandline
|
||||||
my ($pipe) = join(' ', @ARGV) =~ /^(.*)$/iso;
|
my ($pipe) = join(' ', @ARGV) =~ /^(.*)$/iso;
|
||||||
|
@ -472,69 +58,8 @@ if ($pipe && open PIPE, "| $pipe")
|
||||||
close PIPE;
|
close PIPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
my $mail_fields = parse_mail($mail_text);
|
Bugzilla::InMail::process_inmail($mail_text);
|
||||||
|
exit;
|
||||||
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");
|
|
||||||
|
|
||||||
__END__
|
__END__
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,14 @@
|
||||||
_ " won't get sent). This affects all mail sent by $terms.Bugzilla,"
|
_ " won't get sent). This affects all mail sent by $terms.Bugzilla,"
|
||||||
_ " not just $terms.bug updates.",
|
_ " not just $terms.bug updates.",
|
||||||
|
|
||||||
|
enable_inmail_cgi =>
|
||||||
|
"Enable HTTP handler for incoming e-mail (email_in.cgi). " _
|
||||||
|
"<b>IMPORTANT NOTE:</b> 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' " _
|
sendmailnow => "Sites using anything older than version 8.12 of 'sendmail' " _
|
||||||
"can achieve a significant performance increase in the " _
|
"can achieve a significant performance increase in the " _
|
||||||
"UI -- at the cost of delaying the sending of mail -- by " _
|
"UI -- at the cost of delaying the sending of mail -- by " _
|
||||||
|
|
Loading…
Reference in New Issue