Sending email (in Perl)

From Devpit
Jump to: navigation, search

Here's the quick code for robustly sending an email using UTF-8, content encoding, mime types, and attachments.

Preamble

use strict;
use warnings;
use Date::Format ();
use Encode ();
use Mail::Sender;

Starting and ending an SMTP transaction

Use mail_sender_open() and mail_sender_close() to begin and end an SMTP transaction. Note that you can call mail_sender_open() repeatedly and it'll reuse the current connection. When you're done, call mail_sender_close() explicitly.

sub mail_sender_open {
        our $mail_sender;
        unless($mail_sender) {
                # Do this only once to efficiently pipeline messages to your server. However,
                # Mail::Sender recommends closing this during delays between batches.
                $mail_sender = Mail::Sender->new({
                        "keepconnection" => 1,
                        "on_errors" => "die",
                        "smtp" => "smtp.local",
                });
        }
        return $mail_sender;
}
sub mail_sender_close {
        our $mail_sender;
        # Cancel a pending message -- one that hasn't called Close(). Mail::Sender
        # automatically Close()s a message if you don't explicitly Close() or Cancel()
        # it, which is a bad idea. This shouldn't die, but there are cases where a
        # latent error can make this complain about not being connected. The eval will
        # also trap if $mail_sender is undef.
        eval { $mail_sender->Cancel() };
        eval { $mail_sender = undef };  # Could die while closing the SMTP connection.
        return;
}

Sending simple messages

This calls mail_sender_open() and sends a simple message.

my $body = "Main message body";
eval {
        # Send a simple message.
        # Note that we should specify the user's timezone, not the server's.
        my $mail_sender = mail_sender_open();
        local $Mail::Sender::NO_DATE = 1;  # Should be an option to $mail_sender->Open().
        $mail_sender->Open({
                "from" => 'from@example.com',
                "fake_from" => 'From Example <me@example.com>',
                "to" => 'to@example.com',
                "fake_to" => 'To Example <to@example.com>',
                "cc" => ["cc1@example.com", "cc2@example.com"],
                "fake_cc" => 'C. C. One <cc1@example.com>, C. C. Two <cc2@example.com>',
                "bcc" => ["bcc1@example.com", "bcc2@example.com"],
                "subject" => "We like databases.",
                "headers" => Date::Format::time2str("Date: %a, %d %b %Y %H:%M:%S %z (%Z)", time(), Time::Zone::tz2zone("CST6CDT", time())),
                "charset" => "utf-8",  # Lower-case seems important here, in spite of the standard. (Apple's mail client?)
                "ctype" => "text/plain",
                "encoding" => "quoted-printable",
        });
        # The quotes around $body are important. They force Perl to build a temporary
        # string so that encode() doesn't modify $body. Otherwise, on a subsequent
        # iteration, this will send a blank message.
        $mail_sender->SendEnc(Encode::encode("utf-8", "${body}", 1));
        $mail_sender->Close();
};
if($@) {
        my $err = $@;
        # Who knows what state Mail::Sender might be in now. This avoids a peculiar bug
        # that causes Mail::Sender to deadlock on network IO during the next delivery
        # after a half-baked message.
        mail_sender_close();
        die $err;
}

Sending messages with attachments

This calls mail_sender_open() and sends a message with attachments.

eval {
        # Send a message with attachments.
        # Note that we should specify the user's timezone, not the server's.
        my $mail_sender = mail_sender_open();
        local $Mail::Sender::NO_DATE = 1;  # Should be an option to $mail_sender->Open().
        $mail_sender->OpenMultipart({
                "from" => 'from@example.com',
                "fake_from" => 'From Example <me@example.com>',
                "to" => 'to@example.com',
                "fake_to" => 'To Example <to@example.com>',
                "cc" => ["cc1@example.com", "cc2@example.com"],
                "fake_cc" => 'C. C. One <cc1@example.com>, C. C. Two <cc2@example.com>',
                "bcc" => ["bcc1@example.com", "bcc2@example.com"],
                "subject" => "We like databases.",
                "headers" => Date::Format::time2str("Date: %a, %d %b %Y %H:%M:%S %z (%Z)", time(), Time::Zone::tz2zone("CST6CDT", time())),
        });
        $mail_sender->Body({
                "charset" => "utf-8",  # Lower-case seems important here, in spite of the standard. (Apple's mail client?)
                "ctype" => "text/plain",
                "encoding" => "quoted-printable",
        });
        # The quotes around $body are important. They force Perl to build a temporary
        # string so that encode() doesn't modify $body. Otherwise, on a subsequent
        # iteration, this will send a blank message.
        $mail_sender->SendEnc(Encode::encode("utf-8", "${body}", 1));
        foreach my $attachment (@attachments) {
                # $attachment contains mime_type, name, and content.
                if($attachment->{"mime_type"} =~ qr~^text/~is) {
                        $mail_sender->Part({
                                "charset" => "utf-8",
                                "ctype" => $attachment->{"mime_type"},
                                "disposition" => "attachment; filename=\"$attachment->{'name'}\";\r\nContent-ID: $attachment->{'name'}",
                                "encoding" => "quoted-printable",
                        });
                        $mail->SendEnc(Encode::encode("utf-8", "$attachment->{'content'}", 1));
                } else {
                        $mail_sender->Part({
                                "ctype" => $attachment->{"mime_type"},
                                "disposition" => "attachment; filename=\"$attachment->{'name'}\";\r\nContent-ID: $attachment->{'name'}",
                                "encoding" => "base64",
                        });
                        $mail_sender->SendEnc($attachment->{"content"});
                }
                $mail_sender->EndPart();
        }
        $mail_sender->Close();
};
if($@) {
        my $err = $@;
        # Who knows what state Mail::Sender might be in now. This avoids a peculiar bug
        # that causes Mail::Sender to deadlock on network IO during the next delivery
        # delivery after a half-baked message.
        mail_sender_close();
        die $err;
}

Cleaning up

Again, remember to close the connection when you're done. This just cleans up; it won't die.

mail_sender_close();