Mailer.pm 7.19 KB
Newer Older
1 2 3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
#
5 6
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
7 8 9 10 11 12

package Bugzilla::Mailer;

use strict;

use base qw(Exporter);
13
@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker);
14 15

use Bugzilla::Constants;
16
use Bugzilla::Error;
17
use Bugzilla::Hook;
18 19
use Bugzilla::Util;

20 21
use Date::Format qw(time2str);

22
use Encode qw(encode);
23
use Encode::MIME::Header;
24
use Email::Address;
25 26
use Email::MIME;
use Email::Send;
27 28

sub MessageToMTA {
29
    my ($msg, $send_now) = (@_);
30 31
    my $method = Bugzilla->params->{'mail_delivery_method'};
    return if $method eq 'None';
32

33 34 35 36 37
    if (Bugzilla->params->{'use_mailer_queue'} and !$send_now) {
        Bugzilla->job_queue->insert('send_mail', { msg => $msg });
        return;
    }

38 39 40 41 42 43 44 45 46 47 48 49 50 51
    my $email;
    if (ref $msg) {
        $email = $msg;
    }
    else {
        # RFC 2822 requires us to have CRLF for our line endings and
        # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF)
        # directly because Perl translates "\n" depending on what platform
        # you're running on. See http://perldoc.perl.org/perlport.html#Newlines
        # We check for multiple CRs because of this Template-Toolkit bug:
        # https://rt.cpan.org/Ticket/Display.html?id=43345
        $msg =~ s/(?:\015+)?\012/\015\012/msg;
        $email = new Email::MIME($msg);
    }
52 53 54 55 56 57

    # We add this header to uniquely identify all email that we
    # send as coming from this Bugzilla installation.
    #
    # We don't use correct_urlbase, because we want this URL to
    # *always* be the same for this Bugzilla, in every email,
58
    # even if the admin changes the "ssl_redirect" parameter some day.
59
    $email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
60 61 62 63
    
    # We add this header to mark the mail as "auto-generated" and
    # thus to hopefully avoid auto replies.
    $email->header_set('Auto-Submitted', 'auto-generated');
64

65 66 67
    # MIME-Version must be set otherwise some mailsystems ignore the charset
    $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');

68
    # Encode the headers correctly in quoted-printable
69 70 71 72 73
    foreach my $header ($email->header_names) {
        my @values = $email->header($header);
        # We don't recode headers that happen multiple times.
        next if scalar(@values) > 1;
        if (my $value = $values[0]) {
74
            if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) {
75
                utf8::decode($value);
76
            }
77 78 79 80

            # avoid excessive line wrapping done by Encode.
            local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;

81 82
            my $encoded = encode('MIME-Q', $value);
            $email->header_set($header, $encoded);
83 84 85
        }
    }

86 87 88
    my $from = $email->header('From');

    my ($hostname, @args);
89
    my $mailer_class = $method;
90
    if ($method eq "Sendmail") {
91
        $mailer_class = 'Bugzilla::Send::Sendmail';
92 93
        if (ON_WINDOWS) {
            $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
94
        }
95
        push @args, "-i";
96 97 98 99 100 101 102 103
        # We want to make sure that we pass *only* an email address.
        if ($from) {
            my ($email_obj) = Email::Address->parse($from);
            if ($email_obj) {
                my $from_email = $email_obj->address;
                push(@args, "-f$from_email") if $from_email;
            }
        }
104
    }
105 106 107 108
    else {
        # Sendmail will automatically append our hostname to the From
        # address, but other mailers won't.
        my $urlbase = Bugzilla->params->{'urlbase'};
109
        $urlbase =~ m|//([^:/]+)[:/]?|;
110 111 112
        $hostname = $1;
        $from .= "\@$hostname" if $from !~ /@/;
        $email->header_set('From', $from);
113 114 115
        
        # Sendmail adds a Date: header also, but others may not.
        if (!defined $email->header('Date')) {
116
            $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time()));
117
        }
118 119
    }

120 121
    if ($method eq "SMTP") {
        push @args, Host  => Bugzilla->params->{"smtpserver"},
122 123
                    username => Bugzilla->params->{"smtp_username"},
                    password => Bugzilla->params->{"smtp_password"},
124
                    Hello => $hostname, 
125
                    ssl => Bugzilla->params->{'smtp_ssl'},
126
                    Debug => Bugzilla->params->{'smtp_debug'};
127 128
    }

129 130
    Bugzilla::Hook::process('mailer_before_send', 
                            { email => $email, mailer_args => \@args });
131

Byron Jones's avatar
Byron Jones committed
132 133 134 135
    $email->walk_parts(sub {
        my ($part) = @_;
        return if $part->parts > 1; # Top-level
        my $content_type = $part->content_type || '';
136 137 138 139 140
        $content_type =~ /charset=['"](.+)['"]/;
        # If no charset is defined or is the default us-ascii,
        # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
        # XXX - This is a hack to workaround bug 723944.
        if (!$1 || $1 eq 'us-ascii') {
Byron Jones's avatar
Byron Jones committed
141 142 143 144 145 146 147 148 149 150 151 152 153 154
            my $body = $part->body;
            if (Bugzilla->params->{'utf8'}) {
                $part->charset_set('UTF-8');
                # encoding_set works only with bytes, not with utf8 strings.
                my $raw = $part->body_raw;
                if (utf8::is_utf8($raw)) {
                    utf8::encode($raw);
                    $part->body_set($raw);
                }
            }
            $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
        }
    });

155 156 157
    if ($method eq "Test") {
        my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
        open TESTFILE, '>>', $filename;
158 159
        # From - <date> is required to be a valid mbox file.
        print TESTFILE "\n\nFrom - " . $email->header('Date') . "\n" . $email->as_string;
160
        close TESTFILE;
161 162
    }
    else {
163 164
        # This is useful for both Sendmail and Qmail, so we put it out here.
        local $ENV{PATH} = SENDMAIL_PATH;
165
        my $mailer = Email::Send->new({ mailer => $mailer_class, 
166 167 168 169
                                        mailer_args => \@args });
        my $retval = $mailer->send($email);
        ThrowCodeError('mail_send_error', { msg => $retval, mail => $email })
            if !$retval;
170 171 172
    }
}

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
# Builds header suitable for use as a threading marker in email notifications
sub build_thread_marker {
    my ($bug_id, $user_id, $is_new) = @_;

    if (!defined $user_id) {
        $user_id = Bugzilla->user->id;
    }

    my $sitespec = '@' . Bugzilla->params->{'urlbase'};
    $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain
    $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate
    if ($2) {
        $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@'
    }

    my $threadingmarker;
    if ($is_new) {
        $threadingmarker = "Message-ID: <bug-$bug_id-$user_id$sitespec>";
    }
    else {
193 194 195
        my $rand_bits = generate_random_password(10);
        $threadingmarker = "Message-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>" .
                           "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>" .
196 197 198 199 200 201
                           "\nReferences: <bug-$bug_id-$user_id$sitespec>";
    }

    return $threadingmarker;
}

202
1;