Book Home Perl for System AdministrationSearch this book

Chapter 8. Electronic Mail

Contents:

Sending Mail
Common Mistakes in Sending Email
Receiving Mail
Module Information for This Chapter
References for More Information

Unlike the other chapters in this book, this chapter does not discuss how to administer a particular service, technology, or knowledge domain. Instead, we're going to look at how to use email from Perl as a tool for system administration.

Perl can help us in an administrative context with both sending and receiving email. Email is a great notification mechanism: often we want a program to tell us when something goes wrong, provide the results of an automatic process (like a late night cron or scheduler service job), or let us know when something we care about changes. We'll explore how to send mail from Perl for these purposes and then look at some of the pitfalls associated with the practice of sending ourselves mail.

Similarly, we'll look at how Perl can be used to post-process mail we receive to make it more useful to us. Perl can be useful for dealing with spam and managing user questions.

This chapter will assume that you already have a solid and reliable mail infrastructure. We're also going to assume that your mail system, or one that you have access to, uses protocols that follow the IETF specifications for sending and receiving mail. The examples in this chapter will use protocols like SMTP (Simple Mail Transfer Protocol, RFC821) and expect messages to be RFC822-compliant. We'll go over these terms in due course.

8.1. Sending Mail

Let's talk about the mechanics of sending email first and then tackle the more sophisticated issues. The traditional (Unix) Perl mail sending code often looks something like this example from the Perl Frequently Asked Questions list:

# assumes we have sendmail installed
open(SENDMAIL, "|/usr/lib/sendmail -oi -t -odq") or 
  die "Can't fork for sendmail: $!\n";
print SENDMAIL <<"EOF";
From: User Originating Mail <me\@host>
To: Final Destination <you\@otherhost>
Subject: A relevant subject line

Body of the message goes here after the blank line
in as many lines as you like.
EOF
close(SENDMAIL) or warn "sendmail didn't close nicely";

TIP

When the array interpolation rules were changed between Perl Version 4 and Perl Version 5, it broke many scripts that sent mail. Even now, be on the lookout for code like this:

$address = "fred@example.com";

This needs to be changed to one of these lines to work properly:

$address="fred\@example.com";
$address='fred@example.com'; 
$address= join('@', 'fred', 'example.com');

Code that calls sendmail like our example above works fine under many circumstances, but it doesn't work on any operating system that lacks a mail transport agent called "sendmail" installed (e.g., NT or MacOS). On those operating systems, this leaves you with a few choices.

8.1.1. Getting sendmail (or Similar Mail Transport Agent)

On Win32, you're in luck because I know of at least three Win32 ports of sendmail itself:

If you'd like something more lightweight, and are willing to make small modifications to your Perl code to support different command-line arguments, other Win32 programs like these will do the trick:

The advantage of this approach is it offloads much of the mail-sending complexity from your script. A good Mail Transport Agent (MTA) handles the process of retrying a destination mail server if it's unreachable, selecting the right destination server (finding and choosing between Mail eXchanger DNS records), rewriting the headers if necessary, dealing with bounces, and so on. If you can avoid having to take care of all of that in Perl, that's often a good thing.

8.1.2. Using the OS-Specific IPC Framework.

On MacOS or Windows NT, you can drive a mail client using the native interprocess communication (IPC) framework.

I haven't seen any MacOS ports of sendmail, but under MacOS, we can ask Perl to use AppleScript to drive an email client:

$to="someone\@example.com";
$from="me\@example.com";
$subject="Hi there";
$body="message body\n";

MacPerl::DoAppleScript(<<EOC);
tell application "Eudora"

    make message at end of mailbox "out"
       
    -- 0 is the current message
    set field \"from\" of message 0 to \"$from\"
    set field \"to\" of message 0 to \"$to\"
    set field \"subject\" of message 0 to \"$subject\"
    set body of message 0 to \"$body\"
    queue message 0
    connect with sending without checking
    quit
end tell
EOC

This code executes a very simple AppleScript that communicates with the email client Eudora by Qualcomm. The script creates a new message, populates and queues the message for sending, and then instructs Eudora to send its queued messages before quitting.

Another slightly more efficient way to write this same code would be to use the Mac::Glue module we saw in Chapter 2, "Filesystems":

use Mac::Glue ':glue';

$e=new Mac::Glue 'Eudora';
$to="someone\@example.com";
$from="me\@example.com";
$subject="Hi there";
$body="message body";

$e->make(
	new => 'message',
	at => location(end => $e->obj(mailbox => 'Out'))
);

$e->set($e->obj(field => from    => message => 0), to => $from);
$e->set($e->obj(field => to      => message => 0), to => $to);
$e->set($e->obj(field => subject => message => 0), to => $subject);
$e->set($e->prop(body => message => 0), to => $body);

$e->queue($e->obj(message => 0));
$e->connect(sending => 1, checking => 0);
$e->quit;

Under NT, we can use Microsoft's Collaborative Data Objects Library (previously called Active Messaging), an ease-of-use layer built on top of their MAPI (Messaging Application Programming Interface) architecture. To call this library to drive a mail client like Outlook, we could use the Win32::OLE module like so:

$to="me\@example.com";
$subject="Hi there";
$body="message body\n";

use Win32::OLE;

# init OLE, COINIT_OLEINITIALIZE required when using MAPI.Session objects
Win32::OLE->Initialize(Win32::OLE::COINIT_OLEINITIALIZE);
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# create a session object that will call Logoff when it is destroyed
my $session = Win32::OLE->new('MAPI.Session','Logoff');
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# log into that session using the default OL98 Internet Profile
$session->Logon('Microsoft Outlook Internet Settings');
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# create a message object
my $message = $session->Outbox->Messages->Add;
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# create a recipient object for that message object
my $recipient = $message->Recipients->Add;
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# populate the recipient object
$recipient->{Name} = $to;
$recipient->{Type} = 1; # 1 = "To:", 2 = "Cc:", 3 = "Bcc:"

# all addresses have to be resolved against a directory 
# (in this case probably your Address book). Full addresses 
# usually resolve to themselves, so this line in most cases will 
# not modify the recipient object.
$recipient->Resolve(  );
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# populate the Subject: line and message body
$message->{Subject} = $subject;
$message->{Text} = $body;

# queue the message to be sent
# 1st argument = save copy of message
# 2nd argument = allows user to change message w/dialog box before sent
# 3rd argument = parent window of dialog if 2nd argument is True
$message->Send(0, 0, 0);
die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError(  );

# explicitly destroy the $session object, calling $session->Logoff 
# in the process
undef $session;

Unlike the previous example, this code just queues the message to be sent. It is up to the mail client (like Outlook) or transport infrastructure (like Exchange) to periodically initiate message delivery. There is a CDO/AM 1.1 method for the Session object called DeliverNow( ) that is supposed to instruct MAPI to flush all incoming and outgoing mail queues. Unfortunately, it is not available and does not work under some circumstances, so it is not included in the previous code example.

The previous code drives MAPI "by hand" using OLE calls. If you'd like to use MAPI without getting your hands that dirty, Amine Moulay Ramdane has put together a Win32::MAPI module (found at http://www.generation.net/~aminer/Perl/) that can take some of the work out of the process.

Programs that rely on AppleScript/Apple Events or MAPI are equally as non-portable as calling a sendmail binary. They offload some of the work, but are relatively inefficient. They should probably be your methods of last resort.

8.1.3. Speaking to the Mail Protocols Directly

Our final choice is to write code that speaks to the mail server in its native language. Most of this language is documented in RFC821. Here's a basic SMTP (Simple Mail Transport Protocol) conversation. The data we send is in bold:

% telnet example.com 25         -- connect to the SMTP port on example.com
Trying 192.168.1.10 ...
Connected to example.com.
Escape character is '^]'.
220 mailhub.example.com ESMTP Sendmail 8.9.1a/8.9.1; Sun, 11 Apr 1999 15:32:16 -0400 (EDT)
HELO client.example.com        -- identify the machine we are connecting from
                                                                             (can also use EHLO)
250 mailhub.example.com Hello dnb@client.example.com [192.168.1.11], pleased to meet you
MAIL FROM: <dnb@example.com>   -- specify the sender
250 <dnb@example.com>... Sender ok
RCPT TO: <dnb@example.com>     -- specify the recipient
250 <dnb@example.com>... Recipient ok
DATA                           -- begin to send message, note we send several key header lines
354 Enter mail, end with "." on a line by itself
From: David N. Blank-Edelman (David N. Blank-Edelman)
To: dnb@example.com
Subject: SMTP is a fine protocol

Just wanted to drop myself a note to remind myself how much I love SMTP.
     Peace,
       dNb
.                              -- finish sending the message
250 PAA26624 Message accepted for delivery
QUIT                           -- end the session
221 mailhub.example.com closing connection
Connection closed by foreign host.

It is not difficult to script a network conversation like this. We could use the Socket module or even something like Net::Telnet as seen in Chapter 6, "Directory Services". But there are good mail modules out there that make our job easier, like Jenda Krynicky's Mail::Sender, Milivoj Ivkovic's Mail::Sendmail, and Mail::Mailer in Graham Barr's MailTools package. All three of these packages are operating-system-independent and will work almost anywhere a modern Perl distribution is available. We'll look at Mail::Mailer because it offers a single interface to two of the mail-sending methods we've discussed so far. Like most Perl modules written in an object-oriented style, the first step is to construct an instance of new object:

use Mail::Mailer;

$from="me\@example.com";
$to="you\@example.com";
$subject="Hi there";
$body="message body\n";

$type="smtp";
$server="mail.example.com";

my $mailer = Mail::Mailer->new($type, Server => $server) or
  die "Unable to create new mailer object:$!\n";

The $type variable allows you to choose one of the following behaviors:

smtp

Send the mail using the Net::SMTP module (part of Barr's libnet package), available for most non-Unix ports of Perl as well. If you are using MailTools Version 1.13 and above, you can specify the SMTP server name using the => notation as demonstrated above. If not, you will have to configure the server name as part of the libnet install procedure.

mail

Send the mail using the Unix mail user agent mail (or whatever binary you specify as an optional second argument). This is similar to our use of AppleScript and MAPI above.

sendmail

Send the mail using the sendmail binary, like our first method of this section.

You can also set the environment variable PERL_MAILERS to change the default locations used to find the binaries like sendmail on your system.

Calling the open( ) method of our Mail::Mailer object causes our object to behave like a filehandle to an outgoing message. In this call, we pass in the headers of the message as a reference to an anonymous hash:

$mailer->open({From => $from, 
               To => $to, 
               Subject => $subject}) or 
  die "Unable to populate mailer object:$!\n";

We print our message body to this pseudo-filehandle and then close it to send the message:

print $mailer $body;
$mailer->close;

That's all it takes to send mail portably via Perl.

Depending on which $type behavior we choose when using this module, we may or may not be covered regarding the harder MTA issues mentioned earlier. The previous code uses the smtp behavior, which means our code needs to be smart enough to handle error conditions like unreachable servers. As written, it's not that smart. Be sure any production code you write is prepared to deal with these issues.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.