diff --git a/webtools/bugzilla/Bugzilla.pm b/webtools/bugzilla/Bugzilla.pm index 374b06e12a28..5a9d23e14daa 100644 --- a/webtools/bugzilla/Bugzilla.pm +++ b/webtools/bugzilla/Bugzilla.pm @@ -81,6 +81,9 @@ sub init_page { # Some environment variables are not taint safe delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + # Some modules throw undefined errors (notably File::Spec::Win32) if + # PATH is undefined. + $ENV{'PATH'} = ''; # If Bugzilla is shut down, do not allow anything to run, just display a # message to the user about the downtime and log out. Scripts listed in diff --git a/webtools/bugzilla/Bugzilla/Config.pm b/webtools/bugzilla/Bugzilla/Config.pm index 1423c1196c49..0859c5bee310 100644 --- a/webtools/bugzilla/Bugzilla/Config.pm +++ b/webtools/bugzilla/Bugzilla/Config.pm @@ -171,6 +171,19 @@ sub update_params { delete $param->{'enablequips'}; } + # Old mail_delivery_method choices contained no uppercase characters + if (exists $param->{'mail_delivery_method'} + && $param->{'mail_delivery_method'} !~ /[A-Z]/) { + my $method = $param->{'mail_delivery_method'}; + my %translation = ( + 'sendmail' => 'Sendmail', + 'smtp' => 'SMTP', + 'qmail' => 'Qmail', + 'testfile' => 'Test', + 'none' => 'None'); + $param->{'mail_delivery_method'} = $translation{$method}; + } + # --- DEFAULTS FOR NEW PARAMS --- _load_params unless %params; @@ -216,7 +229,7 @@ sub update_params { } if (ON_WINDOWS && !-e SENDMAIL_EXE - && $param->{'mail_delivery_method'} eq 'sendmail') + && $param->{'mail_delivery_method'} eq 'Sendmail') { my $smtp = $answer->{'SMTP_SERVER'}; if (!$smtp) { @@ -233,7 +246,7 @@ sub update_params { } } - $param->{'mail_delivery_method'} = 'smtp'; + $param->{'mail_delivery_method'} = 'SMTP'; } write_params($param); diff --git a/webtools/bugzilla/Bugzilla/Config/MTA.pm b/webtools/bugzilla/Bugzilla/Config/MTA.pm index 53340dc133fe..a9bc4619c892 100644 --- a/webtools/bugzilla/Bugzilla/Config/MTA.pm +++ b/webtools/bugzilla/Bugzilla/Config/MTA.pm @@ -34,6 +34,7 @@ package Bugzilla::Config::MTA; use strict; use Bugzilla::Config::Common; +use Email::Send; $Bugzilla::Config::MTA::sortkey = "10"; @@ -43,10 +44,8 @@ sub get_param_list { { name => 'mail_delivery_method', type => 's', - choices => $^O =~ /MSWin32/i - ? ['smtp', 'testfile', 'sendmail', 'none'] - : ['sendmail', 'smtp', 'qmail', 'testfile', 'none'], - default => 'sendmail', + choices => [Email::Send->new()->all_mailers(), 'None'], + default => 'Sendmail', checker => \&check_mail_delivery_method }, diff --git a/webtools/bugzilla/Bugzilla/Constants.pm b/webtools/bugzilla/Bugzilla/Constants.pm index b8171d1c123d..9aefea429f26 100644 --- a/webtools/bugzilla/Bugzilla/Constants.pm +++ b/webtools/bugzilla/Bugzilla/Constants.pm @@ -103,6 +103,7 @@ use File::Basename; ADMIN_GROUP_NAME SENDMAIL_EXE + SENDMAIL_PATH FIELD_TYPE_UNKNOWN FIELD_TYPE_FREETEXT @@ -290,6 +291,8 @@ use constant ADMIN_GROUP_NAME => 'admin'; # Path to sendmail.exe (Windows only) use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe'; +# Paths to search for the sendmail binary (non-Windows) +use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; # Field types. Match values in fielddefs.type column. These are purposely # not named after database column types, since Bugzilla fields comprise not diff --git a/webtools/bugzilla/Bugzilla/Install/Requirements.pm b/webtools/bugzilla/Bugzilla/Install/Requirements.pm index 153149a080b6..2ecbc9e3c89e 100644 --- a/webtools/bugzilla/Bugzilla/Install/Requirements.pm +++ b/webtools/bugzilla/Bugzilla/Install/Requirements.pm @@ -78,11 +78,6 @@ sub REQUIRED_MODULES { module => 'Template', version => '2.12' }, - { - package => 'MailTools', - module => 'Mail::Mailer', - version => '1.67' - }, { package => 'MIME-Base64', module => 'MIME::Base64', @@ -94,6 +89,17 @@ sub REQUIRED_MODULES { module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser', version => '5.406' }, + { + package => 'Email-Send', + module => 'Email::Send', + version => ON_WINDOWS ? '2.16' : '2.00' + }, + { + # This will pull in Email::MIME for us, also. + package => 'Email-MIME-Modifier', + module => 'Email::MIME::Modifier', + version => 0 + }, ); my $all_modules = _get_extension_requirements( @@ -186,15 +192,6 @@ sub OPTIONAL_MODULES { }, # Inbound Email - { - # Email::MIME::Attachment::Stripper can throw an error with - # earlier versions. - # This also pulls in Email::MIME and Email::Address for us. - package => 'Email-MIME-Modifier', - module => 'Email::MIME::Modifier', - version => '1.43', - feature => 'Inbound Email' - }, { package => 'Email-MIME-Attachment-Stripper', module => 'Email::MIME::Attachment::Stripper', diff --git a/webtools/bugzilla/Bugzilla/Mailer.pm b/webtools/bugzilla/Bugzilla/Mailer.pm index 2105d38ae53d..633964b33848 100644 --- a/webtools/bugzilla/Bugzilla/Mailer.pm +++ b/webtools/bugzilla/Bugzilla/Mailer.pm @@ -28,6 +28,7 @@ # Gervase Markham # Byron Jones # Frédéric Buclin +# Max Kanat-Alexander package Bugzilla::Mailer; @@ -37,181 +38,76 @@ use base qw(Exporter); @Bugzilla::Mailer::EXPORT = qw(MessageToMTA); use Bugzilla::Constants; +use Bugzilla::Error; use Bugzilla::Util; -use Mail::Header; -use Mail::Mailer; -use Mail::Address; -use MIME::Parser; -use MIME::QuotedPrint; -use MIME::Base64; - +use Encode qw(encode); +use Email::MIME; +# Loading this gives us encoding_set. +use Email::MIME::Modifier; +use Email::Send; sub MessageToMTA { my ($msg) = (@_); - my $params = Bugzilla->params; - return if ($params->{'mail_delivery_method'} eq "none"); + my $method = Bugzilla->params->{'mail_delivery_method'}; + return if $method eq 'None'; - my ($header, $body) = $msg =~ /(.*?\n)\n(.*)/s ? ($1, $2) : ('', $msg); - my $headers; - - if ($params->{'utf8'} - and (!is_7bit_clean($header) or !is_7bit_clean($body))) - { - ($headers, $body) = encode_message($msg); - } else { - my @header_lines = split(/\n/, $header); - $headers = new Mail::Header \@header_lines, Modify => 0; + my $email = ref($msg) ? $msg : Email::MIME->new($msg); + foreach my $part ($email->parts) { + $part->charset_set('UTF-8') if Bugzilla->params->{'utf8'}; + $part->encoding_set('quoted-printable') if !is_7bit_clean($part->body); } - # Use trim to remove any whitespace (incl. newlines) - my $from = trim($headers->get('from')); - - if ($params->{"mail_delivery_method"} eq "sendmail" && $^O =~ /MSWin32/i) { - my $cmd = '|' . SENDMAIL_EXE . ' -t -i'; - if ($from) { - # We're on Windows, thus no danger of command injection - # via $from. In other words, it is safe to embed $from. - $cmd .= qq# -f"$from"#; + # Encode the headers correctly in quoted-printable + foreach my $header qw(From To Cc Reply-To Sender Errors-To Subject) { + if (my $value = $email->header($header)) { + my $encoded = encode('MIME-Q', $value); + $email->header_set($header, $encoded); } - open(SENDMAIL, $cmd) || - die "Failed to execute " . SENDMAIL_EXE . ": $!\n"; - print SENDMAIL $headers->as_string; - print SENDMAIL "\n"; - print SENDMAIL $body; - close SENDMAIL; - return; } - my @args; - if ($params->{"mail_delivery_method"} eq "sendmail") { + my $from = $email->header('From'); + + my ($hostname, @args); + if ($method eq "Sendmail") { + if (ON_WINDOWS) { + $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE; + } push @args, "-i"; - if ($from) { - push(@args, "-f$from"); - } - } - if ($params->{"mail_delivery_method"} eq "sendmail" - && !$params->{"sendmailnow"}) - { - push @args, "-ODeliveryMode=deferred"; - } - if ($params->{"mail_delivery_method"} eq "smtp") { - push @args, Server => $params->{"smtpserver"}; - if ($from) { - $ENV{'MAILADDRESS'} = $from; - } - } - my $mailer = new Mail::Mailer($params->{"mail_delivery_method"}, @args); - if ($params->{"mail_delivery_method"} eq "testfile") { - $Mail::Mailer::testfile::config{outfile} = - bz_locations()->{'datadir'} . '/mailer.testfile'; - } - - $mailer->open($headers->header_hashref); - print $mailer $body; - $mailer->close; -} - -sub encode_message { - my ($msg) = @_; - - my $parser = MIME::Parser->new; - $parser->output_to_core(1); - $parser->tmp_to_core(1); - my $entity = $parser->parse_data($msg); - $entity = encode_message_entity($entity); - - my @header_lines = split(/\n/, $entity->header_as_string); - my $head = new Mail::Header \@header_lines, Modify => 0; - - my $body = $entity->body_as_string; - - return ($head, $body); -} - -sub encode_message_entity { - my ($entity) = @_; - - my $head = $entity->head; - - # encode the subject - - my $subject = $head->get('subject'); - if (defined $subject && !is_7bit_clean($subject)) { - $subject =~ s/[\r\n]+$//; - $head->replace('subject', encode_qp_words($subject)); - } - - # encode addresses - - foreach my $field (qw(from to cc reply-to sender errors-to)) { - my $high = $head->count($field) - 1; - foreach my $index (0..$high) { - my $value = $head->get($field, $index); - my @addresses; - my $changed = 0; - foreach my $addr (Mail::Address->parse($value)) { - my $phrase = $addr->phrase; - if (is_7bit_clean($phrase)) { - push @addresses, $addr->format; - } else { - push @addresses, encode_qp_phrase($phrase) . - ' <' . $addr->address . '>'; - $changed = 1; - } - } - $changed && $head->replace($field, join(', ', @addresses), $index); - } - } - - # process the body - - if (scalar($entity->parts)) { - my $newparts = []; - foreach my $part ($entity->parts) { - my $newpart = encode_message_entity($part); - push @$newparts, $newpart; - } - $entity->parts($newparts); + push(@args, "-f$from") if $from; + push(@args, "-ODeliveryMode=deferred") + if !Bugzilla->params->{"sendmailnow"}; } else { - # Extract the body from the entity, for examination - # At this point, we can rely on MIME::Tools to do our encoding for us! - my $bodyhandle = $entity->bodyhandle; - my $body = $bodyhandle->as_string; - if (!is_7bit_clean($body)) { - # count number of 7-bit chars, and use quoted-printable if more - # than half the message is 7-bit clean - my $count = ($body =~ tr/\x20-\x7E\x0A\x0D//); - if ($count > length($body) / 2) { - $head->mime_attr('Content-Transfer-Encoding' => 'quoted-printable'); - } else { - $head->mime_attr('Content-Transfer-Encoding' => 'base64'); - } - } - - # Set the content/type and charset of the part, if not set - $head->mime_attr('Content-Type' => 'text/plain') - unless defined $head->mime_attr('content-type'); - $head->mime_attr('Content-Type.charset' => 'UTF-8'); + # Sendmail will automatically append our hostname to the From + # address, but other mailers won't. + my $urlbase = Bugzilla->params->{'urlbase'}; + $urlbase =~ m|//([^/]+)/?|; + $hostname = $1; + $from .= "\@$hostname" if $from !~ /@/; + $email->header_set('From', $from); } - $head->mime_attr('MIME-Version' => '1.0'); - $head->fold(75); - return $entity; -} - -sub encode_qp_words { - my ($line) = (@_); - my @encoded; - foreach my $word (split / /, $line) { - if (!is_7bit_clean($word)) { - push @encoded, '=?UTF-8?Q?_' . encode_qp($word, '') . '?='; - } else { - push @encoded, $word; - } + if ($method eq "SMTP") { + push @args, Host => Bugzilla->params->{"smtpserver"}, + Hello => $hostname; + } + + if ($method eq "Test") { + my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; + open TESTFILE, '>>', $filename; + print TESTFILE "\n\n---\n\n" . $email->as_string; + close TESTFILE; + } + else { + # This is useful for both Sendmail and Qmail, so we put it out here. + local $ENV{PATH} = SENDMAIL_PATH; + my $mailer = Email::Send->new({ mailer => $method, + mailer_args => \@args }); + my $retval = $mailer->send($email); + ThrowCodeError('mail_send_error', { msg => $retval, mail => $email }) + if !$retval; } - return join(' ', @encoded); } 1; diff --git a/webtools/bugzilla/template/en/default/admin/params/mta.html.tmpl b/webtools/bugzilla/template/en/default/admin/params/mta.html.tmpl index 4089820b5ec7..224d215447ba 100644 --- a/webtools/bugzilla/template/en/default/admin/params/mta.html.tmpl +++ b/webtools/bugzilla/template/en/default/admin/params/mta.html.tmpl @@ -28,14 +28,13 @@ mail_delivery_method => "Defines how email is sent, or if it is sent at all.
  • - 'sendmail', 'smtp' and 'qmail' are all MTAs. + 'Sendmail', 'SMTP' and 'Qmail' are all MTAs. You need to install a third-party sendmail replacement if you want to use sendmail on Windows.
  • - 'testfile' is useful for debugging: all email is stored - in 'data/mailer.testfile' instead of being sent. For more - information, see the Mail::Mailer manual. + 'Test' is useful for debugging: all email is stored + in 'data/mailer.testfile' instead of being sent.
  • 'none' will completely disable email. $terms.Bugzilla continues diff --git a/webtools/bugzilla/template/en/default/global/code-error.html.tmpl b/webtools/bugzilla/template/en/default/global/code-error.html.tmpl index f6ccae7548e1..a0e9bd9b9286 100644 --- a/webtools/bugzilla/template/en/default/global/code-error.html.tmpl +++ b/webtools/bugzilla/template/en/default/global/code-error.html.tmpl @@ -295,6 +295,11 @@ [% ELSIF error == "ldap_server_not_defined" %] The LDAP server for authentication has not been defined. + [% ELSIF error == "mail_send_error" %] + There was an error sending mail from '[% mail.header('From') FILTER html %]' + to '[% mail.header('To') FILTER html %]': + [% msg FILTER html %] + [% ELSIF error == "missing_bug_id" %] No [% terms.bug %] ID was given.