PREVIOUS  TABLE OF CONTENTS  NEXT 

Creating, Processing and Sending Mail

Graham Barr

It's hard to imagine the Internet without electronic mail. E-mail is--usually--nothing more than a piece of text; that's why Perl is an excellent tool for processing it. You can use Perl to generate mail, or to sort it, or to write a mail filter which can automatically reply to messages - handy for those times when you're away and want to let people know.

The examples in this column all use the Mail::Internet package, which started life as a mail filter written in Perl 4. When Perl 5 came along it was made more general so that it could be used for program that needed to process, and not merely filter, e-mail. It's now part of the MailTools distribution, which is available from the CPAN. The MailTools distribution contains several other packages for e-mail related activities, such as Mail::Address (for address parsing), Mail::Mailer and Mail::Send (for sending mail), and Mail::Util (which contains some generic utilities). The perlbug program, which now comes bundled with Perl, is a good example of their use.

There are many different flavors of e-mail - the "Internet standard" is just one type, and it's what this article will focus on. Internet mail is described in RFC-822, accessible from one of the sites in

http://www.yahoo.com/Computers_and_Internet/Standards/RFCs/

Creating Mail

E-mail is made up from two parts: the header, which is a list of structured header lines, and the body, which is the text being sent. They're separated by a blank line - that's "\n\n" to you.

First let's look at creating an object in Perl which can be used to represent an e-mail message. An empty message can be created by calling the constructor of the Mail::Internet class.

#!/usr/bin/perl 

 # Create a new Mail::Internet object,
 # which we'll use to build our message. 

 $mail = Mail::Internet->new();

Now that we have our Mail object, we can begin building the message. First we need to add some header lines, such as:

To: List of recipients 
Cc: List of Carbon Copy recipients 
Bcc: List of Blind Carbon Copy recipients 
Subject: Short description of the message

The Bcc line won't be visible in the received message. (Useful for sending your boss copies!)

Perhaps you've noticed strange lines starting with "X-" in messages you've received. These are user-defined headers; the RFC-822 standard allows for any header you like, as long as it begins with that X-. Here are some commonly used headers:

X-Mailer: The name of the mailer which sent this message 
X-Sun-Charset: The character set used (e.g. US-ASCII ) 
X-Sender: The address of the sender
X-Authentication-Warning: Warnings from some SMTP daemons 

You can also make up your own:

X-Caffeination-Level: High 
X-Spoiler: Rosebud is a sled. 
X-Haiku: Old man seeks doctor/ "I eat SPAM daily," says he/ Angioplasty.

But to build our message we often just need to know three things: who we're sending the message to, who we want to carbon copy, and the subject line.

# Add three fields to the message:
# To:, Cc:, and Subject:

$mail->add(To => 'user1@foobar.com', 
              Cc => 'xyz@not.here', 
              Subject => 'Please Read This ...' 
             ); 

And now our header is built - all that's left is to add the message text, with the body() method. The Mail::Internet package stores the text in a array, with each item representing a single line of the text. So assuming that the text has been read into @text, we can place it in the body of the mail object as follows:

# Store @text in the message body.
# The $mail object makes a copy, so later 
# changes to @text won't affect the
# message. 

$mail->body( \@text ); 

or you can use an anonymous array instead:

$mail->body( ["Be sure to return", "by midnight", " -Fairy Godmother"] );

It's possible that you might not want to use a script to construct e-mail. For example, you might have a preformatted message stored in a file. Or perhaps your script is a filter for messages made available via STDIN.

For these situations, you can use Mail::Internet to read a message from a file descriptor, or from an array:

# To read a message from STDIN 

$mail = Mail::Internet->new( \*STDIN ); 

# To read from STDIN or a file in @ARGV 

$mail = Mail::Internet->new( [ <> ] ); 

Processing Mail

Now that we have our $mail object, we can process it however we wish: for mailing lists, vacation filters, mailftp, replyto, and so on.

As an example of processing mail, let's write a simple vacation filter which sends automated replies to anyone who sends us mail, explaining that we're not available to respond.

First our program needs to read the incoming message and create an empty message for the reply:

#!/usr/bin/perl 

require Mail::Internet; 

# We'll need Mail::Address to extract the 
# return address from the incoming message 

require Mail::Address; 

# Since we're running as a filter, the
# incoming message will be available from
# STDIN: 

$mail = Mail::Internet->new( \*STDIN ); 

# Create an empty Mail::Internet object
# for our reply 

$reply = Mail::Internet->new(); 

Next we need to specify the recipient. There are several different header lines we can look in for a reply address, but for simplicity I'll just use the common ones: From and Reply-To.

# Locate address of sender. The get()
# method returns a header line from the
# message, or undef if it doesn't exist. 

$sender = $mail->get('Reply-To') || 
          $mail->get('From'); 

$sender now contains the return address of the person who sent the original message. This is probably sufficient, but for demonstration purposes we'll use Mail::Address to create a To header that we know to be correct:

# Create a Mail::Address object 
# Mail::Address->parse returns a list of
# objects. We probably want the first. 

$sender = (Mail::Address->parse($sender))[0]; 

$reply->add(To => $sender->format); 
There are two header lines that we ought to add now: Subject and References. For the Subject line we'll simply add "Re:" to the beginning of the original subject, unless it's already there. The References line (an ordered list of previous message IDs) isn't essential, but is often used by "threading" mailers to group related messages together. The Message-Id header field contains a unique ID and is added by your mailer dæmon (e.g. sendmail).
# Extract the Subject from the original 
# message 

$subject = $mail->get('Subject'); 

if ($subject) { 

# Add Re: prefix 
    $subject = 'Re: ' . $subject 
              unless $subject =~ /^\s*Re:/i; 

# Set the Subject line on our new message 

    $reply->add(Subject => $subject); 
} 

# Extract the References and Message-Id from 
# the original message 

$ref = $mail->get('References') || ''; 
$mid = $mail->get('Message-Id'); 

# Combine them to create our new 
# References line 

$ref .= ' ' . $mid if $mid; 
# Add our new References line to our 
# reply message 

$reply->add(References => $ref) 
                       if length $ref; 

Now that our header is built, we'll add a body. I'll just add a hardcoded blob of text, but it could have been read from a file (such as $ENV{HOME}/.vacation).

# Create an array containing reply text 

@reply = ("This is a recorded message", 
          "I am away from my machine at the moment", 
          "and will reply to your message",
          "as soon as I return"); 

# Add the body to the reply message 

$reply->body( \@reply ); 

Now that our message is complete, we just need to send it. This can be done in several ways. If you're on a UNIX system, you can open a pipe to sendmail and print the message through the pipe. For example:

# Open a pipe to /usr/lib/sendmail. -t tells
# sendmail to scan the input for To, Cc, and 
# Bcc headers, and extract the mail addresses. 

open(MAIL, "|/usr/lib/sendmail -t") 
                 || die "Can't send mail: $!"; 

# print the message down the pipe 
# (i.e.: send the message to sendmail)

$reply->print( \*MAIL ); 

close(MAIL);

The message has now been sent (hopefully!).

Obviously, if you don't have sendmail, or an equivalent program, this isn't an option. Then you have to send the message directly to a mailhost machine using the Simple Mail Transfer Protocol (SMTP), defined in RFC821.

SMTP, being a text-based protocol, is easily implemented in Perl. In fact, a Net::SMTP module has already been written. But before using this module, there are a few things you need to know:

require Net::SMTP; 
use Mail::Util qw(mailaddress); 

# Create a Net::SMTP object, which opens 
# an SMTP connection to the mailhost. 
# This sends the SMTP 'HELO' command. 

$smtp = Net::SMTP->new('mailhost'); 

# Tell the SMTP server we want to start to send 
# a piece of mail, use mailaddress() to find 
# our own e-mail address, which will be used
# as a return address.
$smtp->mail( mailaddress() ) 
                       || die $smtp->message; 

# Extract all the recipients from the reply
my @rcpt = ($reply->get('To', 'Cc', 'Bcc')); 

# use Address::parse to parse these addresses,
# returning a list of Mail::Address objects. 
# Then map these, ending up with a list
# of addresses.
my @addr = map($_->address, 
               Mail::Address->parse(@rcpt)); 

# Tell the SMTP server the recipient 
# addresses.

$smtp->to(@addr) || die $smtp->message; 

# Create the message as a single piece of text, 
# ensuring that there's a blank line between 
# the header and the body. 
my $text = join("", @{$reply->header},
                "\n", @{$reply->body}); 

# Send the message to the SMTP server 

$smtp->data( $text ) || die $smtp->message; 
# Tell the SMTP server that we've finished 
# ...and close the connection 

$smtp->quit;

The message has now been sent. Mail::Internet provides a method for doing all of this: smtpsend() sends SMTP messages for you. Mail::Internet also defines a similar method, nntppost(), which (assuming the object has a Newsgroups header) posts the message to your news host using NNTP, the Network News Transfer Protocol.

That example could be split into two scripts, one which generates the reply and one which sends it. The reply script could then be enhanced so that the body is a duplicate of the incoming message indented - ideal as a reply generator. The code to do this could look something like:

# Create a duplicate of the original 
# body text 

$body = [ @{$mail->body} ]; 

# Prefix each line with a > 

grep { s/\A/>/ } @$body; 

# $sender, which was set earlier in the
# script, contains a Mail::Address
# object. From this we may be able to
# extract the senders name. If not,
# default to the senders address.

$name = $sender->name || 
                       $sender->address; 

# Add a one line prefix to the message
# stating who we're quoting 

unshift(@$body, $name . " Wrote:\n"); 

# Set the body text of our reply message 
$reply->body( $body ); 
And we're done!

Many people complain about being flooded with e-mail. For the most part, that's e-mail sent by people. Now that our programs can generate e-mail as well, we can expect the floodgates to widen, right? Possibly - but our programs can filter e-mail too, letting us narrow the floodgates if we wish.

__END__


PREVIOUS  TABLE OF CONTENTS  NEXT