Programming with Perl Modules: Chapter 4The Mail and MIME Modules
Electronic mail is arguably the most essential Internet application. Although there are many different mail clients, standards for mail delivery and retrieval simplify the process of writing a mail client to send and/or retrieve mail. The Send Mail Transfer Protocol, or SMTP, is responsible for clients negotiating RCPT ("to") and FROM ("from") requests with the SMTP server, then sending data to the SMTP server, followed by an EOF. The Post Office Protocol Version 3, or POP3, was designed to retrieve electronic mail from a server after a user has authenticated with a username and password. Authenticated users can retrieve any information about their mailboxes, including the number of messages, headers, and message bodies, and specific messages by entering the message number.
You'll find that many of the This chapter covers modules designed for sending and retrieving email, as well as "helper" modules for more rudimentary message handling. We also cover the MIME and encoding modules used to package data for mail transfer and translate data upon receipt.
Send Email with Mail::MailerThe Mail::Mailer module interacts with external mail programs. When you "use" Mail::Mailer or create a new Mail::Mailer object, you can specify which mail program you want your program to talk to:
use Mail::Mailer qw(mail);You can also specify the mail program that sends the mail:
use Mail::Mailer; $type = 'sendmail'; $mailprog = Mail::Mailer->new($type); $type is the mail program you'll be using. Once you've created
a new object, you use the open() function to send the message
headers to the mail program, where %headers is a hash of key/value
pairs that represent the header type and the value of the header:
# mail headers to use in the message
%headers = (
'To' => 'nvp@mail.somename.com',
'From' => 'nvp@mail.somename.com',
'Subject' => 'working?'
);
This code represents a set of headers where the recipient of the mail message
is nvp@mail.somename.com, the mail was sent from
nvp@mail.somename.com, and the subject of the mail message is "working?"
Once
$mailprog->open(\%headers);The body of the message is then sent to the mail program:
print $mailprog "This is the message body.\n";Now, close the program when the message is finished.
$mailprog->close;Let's put it all together. A practical example of using Mail::Mailer would be a command-line-driven application that works much like the UNIX mail program, reading STDIN until EOF or mailing
a file specified on the command line, like so:
mailprogram filenameThe code for this program might look like:
#!/usr/local/bin/perl -w
use Mail::Mailer;
use strict; # or forever hold your peace
my($mailer, $body);
my(%headers);
# use sendmail for mailing
$mailer = Mail::Mailer->new('sendmail');
# mail headers to use in the message
%headers = (
'To' => 'nvp@mail.somename.com', # the recipient
'From' => 'nvp@mail.somename.com', # the sender
'Subject' => 'working?' # the subject
);
# send headers to the mail program
$mailer->open(\%headers);
# undefine record separator, this is necessary
# to assign <> to a scalar
undef $/;
# read <> for body of message
$body = <>;
# send $body to the mail program
print $mailer $body;
# end session
$mailer->close;
Better Header Control with Mail::SendMail::Send is built on top of Mail::Mailer, which means that you can also choose the mail program that sends the mail. Mail::Mailer has implemented functionsto(), cc(), bcc(),
and subject() to replace the %headers
hash used in Mail::Mailer.
Mail::Send uses the
# Start mailer and output headers
$fh = $msg->open('sendmail');
serves the same purpose as:
# use sendmail for mailing
$mailer = Mail::Mailer->new('sendmail)';
This code tells Mail::Send to use sendmail
as the mail program. Here's a rewritten example of the
above, which takes command-line arguments for subject
and recipient, but still reads from <>:
mailprogram -t <recipient> -subj <subject> <filename>The program is written as:
#!/usr/local/bin/perl -w
use strict; # or I'm coming to your house to sell you some knives
use Getopt::Long;
my($recip, $subj, $fh, $msg, $body); # for strict
require Mail::Send;
# $recip is the variable for the -t switch
# $subj is the variable for the -subj switch
# both switches are required or output a usage message
GetOptions('t=s', \$recip, 'subj=s', \$subj);
# we have both switches
if($recip && $subj) {
$msg = Mail::Send->new();
# who the message is for
$msg->to($recip);
# the subject of the message
$msg->subject($subj);
# Start mailer and output headers
$fh = $msg->open('sendmail');
# undefine record separator so scalar can be
# assigned to <> instead of an array
undef $/;
# read <>
$body = <>;
# send the body of the message to the mailer
print $fh $body;
# complete the message and send it
$fh->close;
# something's missing. give usage message and exit.
} else {
&usage();
exit(0);
}
# the usage message
sub usage {
print("Usage: $0 -t <recipient> -subj
<subject> <filename>\n");
}
Mail::Send also provides the set() and
CODE>add() functions, which assign a value to a
header tag and append a value to a header tag. The
set() function takes two arguments: a
header tag and a value. The set() function
is used like this:
$msg->set($scalar, @array);Therefore, to address a message to nvp@mail.somename.com:
$msg->set('To', 'nvp@mail.somename.com');
The above sets the To header to
nvp@mail.somename.com; however, the following
sets the To header to postmaster@mail.somename.com
and nvp@mail.somename.com, because they represent an array of
values.
$msg->set('To', ('nvp@mail.somename.com', 'postmaster@mail.somename.com'));
The set() function can't be used to add multiple values to
a header value, like:
$msg->set('To', 'nvp@mail.somename.com');
$msg->set('To', 'someone@their.mailaddress.com');
The set() function doesn't append information from one
call to another; the previous case only addresses mail to
someone@their.mailaddress.com. The add
function is used for this purpose. For example:
$msg->add('To', 'nvp@mail.somename.com');
$msg->add('To', 'someone@their.mailaddress.com');
This example addresses mail to both parties.
Rewriting the Mail::Send example shown above to use
# @ recip from GetOptions('t=s', \@recip);
$msg->add('To', @recip);
# $subj from GetOptions('subj=s', \$subj);
$msg->set('Subject', $subj);
Putting it all together, mail can be sent to multiple recipients
using Mail::Send with the following example:
#!/usr/local/bin/perl -w
# strict checking is our friend. although it's not good
# to use one's friends, it's always good to use strict. :-)
use strict;
use Getopt::Long;
my(@recip, $subj, $fh, $msg, $body); # for strict
require Mail::Send;
# @recip is an array of recipients - at least one recipient
# is required. $subj is the subject of the message (this is
# also required)
GetOptions('t=s', \@recip, 'subj=s', \$subj);
# both switches have been entered
if((scalar(@recip) > 0) && $subj) {
# new Mail::Send object
$msg = Mail::Send->new();
# Add recipients to the list
$msg->add('To', @recip);
# Set the subject of the message
$msg->set('Subject', $subj);
# Start mailer and output headers;
# in this case, use sendmail.
$fh = $msg->open('sendmail');
# undefine record separator so we can use a scalar
undef $/;
# read <> into $body
$body = <>;
# send the message body to the mailer
print $fh $body;
# complete the message and send it
$fh->close;
} else {
usage();
}
sub usage {
print("Usage: $0 -t <recipient> -subj <subject>
<filename>\n");
exit(0);
}
Interface to SMTP with Net::SMTPNet::SMTP, a subclass of Net::Cmd and IO::Socket::INET, was written by Graham Barr (gbarr@ti.com) and is included with thelibnet distribution. Net::SMTP
implements an interface to the SMTP and ESMTP protocols, which send
mail by talking to an SMTP server (through a socket) as described
in RFC 821.When would you want to use Net::SMTP instead of sending mail with an external program? Since socket communications through sockets don't involve spawning an external program, your programs won't suffer from the overhead associated with running an extra process. Talking to SMTP is convenient in cases of sending a volume of messages through electronic mail. Naturally, your server must have an SMTP server running or a remote mailhost must allow you to talk to it; otherwise you won't be able to use this module. If your server (or a remote host) doesn't have an SMTP server running, you'll be forced to use an external program to send your email with Mail::Mailer or Mail::Send; such is the case with home computers, which don't generally run their own SMTP server.
The SMTP Protocol and the SMTP SessionThe SMTP protocol defines a set of commands a client sends to an SMTP server. The SMTP server is generally bound to port 25 of a mailhost. This section walks you through an SMTP session by showing you how the requests and responses are negotiated between the client and the server.When a client negotiates an SMTP session with a server, the server tells the client that it's listening:
Trying 127.0.0.1 ... Connected to mailhost.somename.com. Escape character is '^]'. 220-mailhost.somename.com Spammers Beware Sendmail 8.6.13/8.6.11 ready at Thu, 11 Sep 1997 21:07:36 -0400 220 ESMTP spoken hereThe server returns a 220 message if everything is OK.
If there is no server running on port 25, you'd receive an error
message that your connection to port 25 was refused.
After you've connected to port 25 of the server, you introduce
yourself to the server by issuing a
HELO 250 mail.somename.com Hello some-remote-host.com [127.0.0.1], pleased to meet youOnce you've been greeted by the server, you'll have to tell the server who the sender of the message is by sending the MAIL
command. The MAIL command takes the string From:
user@hostname as an argument. If the MAIL command
is successful, the server sends a 250 response:
MAIL From: realuser@realhost.com 250 realuser@realhost.com... Sender okThen tell the server who you're sending the message to with RCPT.
If the RCPT command is successful, the server sends a
250 response:
RCPT To: nospam@rid-spam-now.com 250 nospam@rid-spam-now.com ... Recipient okNow you're ready to send the body of your message to the server. The DATA command tells the server that all the data until the
. on a line by itself will be received as the body of the
email message.
DATA 354 Enter mail, end with "." on a line by itself Subject: Hi, just thought you'd be interested ... Hi, sorry to bother you, but I figured that you wouldn't mind sparing a moment of your time to read about something disgusting, explicit, or downright irritating blah blah blah ... Please reply to my email address with the Subject: REMOVE so we know that your email address is valid and can send you more junk mail in the future. . 250 VAA09505 Message accepted for deliveryOnce the message has been accepted for delivery (with a 250
response), you can exit the SMTP session with the QUIT command,
which returns 221 on success:
QUIT 221 mail.somename.com closing connection Connection closed by foreign host. The Net::SMTP Interface to SMTPThe Net::SMTP methods are named after the SMTP command they perform. This section introduces you to these methods and walks you through a complete SMTP session using Net::SMTP.
Create a new object with new()Before you can negotiate an SMTP session with a mailhost, you must create a new object withnew(). All SMTP commands are
accessed through this object. The new() method takes
the hostname of the mail server and options for the SMTP connection
(see below) as arguments. The other options are passed to
new() as a hash (name/value pairs).
Here are the options that can be passed to
Timeout
Debug
$mailhost, that times out in 30 seconds if a
connection can't be made with the SMTP server and outputs
debugging information:
$mailhost = 'mail.someplace.com';
$smtp = Net::SMTP->new($mailhost,
Hello => $mailhost,
Timeout => 30,
Debug => 1,
);
The following Net::SMTP methods each return a value if successful,
and undef otherwise.
hello()
$smtp->hello('my-maildomain-com');
mail()
$smtp->mail('me@my-domain.com');
mail() initiates the message sending process.
@mailto = ('she@her.com', 'him@his.com');
# send a separate To: line for each recipient
for(@mailto) { $smtp->recipient($_); }
to()
data()
datasend()
dataend()
@list_data = (1..10); $smtp->data(); $smtp->datasend(@list_data); $smtp->dataend(); help()
$subject = 'HELO'; $help_text = $smtp->help($subject); print $help_text."\n" if $help_text; quit()
#!/usr/local/bin/perl -w
use Net::SMTP;
use strict;
my($mailhost, $sender, $recipient);
# mailhost, sender, and recipient declarations
$mailhost = 'smtp.my-mailhost.com';
$sender = 'me@mail.somesite.com';
$recipient = 'webmaster@www.my-favorite-site.com';
# constructor
$smtp = Net::SMTP->new($mailhost);
# our sender and recipient
$smtp->mail($sender);
$smtp->to($recipient);
# start sending the DATA command to the server
$smtp->data();
# now, send the data to the server
$smtp->datasend(<<END);
To: $recipient
Subject: how are you enjoying your log?
Mine's pretty woody.
Love,
$sender
END
# stop sending data to the server
$smtp->dataend();
# now, quit the connection
$smtp->quit;
Read Email with Mail::POP3ClientMany networks have machines dedicated to sending and receiving electronic mail. Since users might hold accounts on foo.bar.com, and mail is sent to pop-server.bar.com, there must be a means to transfer this mail from the "post office machine" to the host that the user works on every day. The Post Office Protocol, or POP, negotiates this mail transfer from machine A to machine B.The machine that runs POP has a server bound to port 109 or 110 (or others) depending on the machine's configuration. When a user wants to retrieve his or her mail, the user's mail client connects to the POP server and authenticates the user with a login name and password. After the user is authenticated, the user can list, read, and delete messages from the POP server. This section introduces you to the important aspects of Mail::POP3Client and offers some examples using this module in the context of the command line. In Chapter 14, Examples, we also show an application using Mail::POP3Client to read electronic mail from a web page. The Mail::POP3Client module simplifies the process of "talking POP" by implementing a number of functions to login, parse, and read mail messages held on the POP server. POP "vitals," such as login name (required), password (required), POP host, port, and debugging flag are passed to the constructor when a new POP3Client object is created. For example:
#!/usr/local/bin/perl -w
use Mail::POP3Client;
$pop = Mail::POP3Client->new("login", # required
"password", #required
"pophost.your.domain", # not required
port, # default is 110
debug_flag); # any positive integer
Counting MessagesTheCount() function represents the number of messages
in the mailbox: if authentication fails, Count() returns
-1; otherwise, it returns 0 or a positive
integer indicating the number of messages in the mailbox. Once
authenticated, a user can list the headers of the messages in
their mailbox using the Count() function, as shown in the following
example:
#!/usr/local/bin/perl -w
use strict; # i will eat it very quick i will eat it using strict
use Mail::POP3Client;
my($pop, $num_mesg, $i);
$pop = Mail::POP3Client->new("nvp",
"xxxxxx",
"mail.somename.com",
110,
1);
# How many messages do we have?
$num_mesg = $pop->Count;
print("You have ".$num_mesg." new message(s).\n");
# Now, output the headers for all the messages.
for ($i = 1; $i <= $num_mesg; $i++) {
print $pop->Head($i), "\n";
}
You can also use a regular expression to parse the headers to show
wanted information, like sender and/or subject of each mail message:
#!/usr/local/bin/perl -w
use strict; # be there or be square
use Mail::POP3Client;
my($pop, $num_mesg, $i);
$pop = Mail::POP3Client->new("nvp", # required
"xxxxxx", # required
"mail.somename.com", # pop host
110, # port (an integer)
1); # debug flag (an integer)
$num_mesg = $pop->Count;
print("You have ".$num_mesg." new message(s).\n");
for ($i = 1; $i <= $pop->Count; $i++) {
foreach ($pop->Head($i)) {
# output from and subject
# if matched by regexp
print $_." " if /^(From|Subject)/;
}
# now, send a trailing \n for grins
print "\n";
}
The above code outputs:
~/ORA/PRK/Chapters/Mail/scripts> ./ex1_7.pl POP3: POPStat at ./ex1_7.pl line 8 You have 1 new message(s). TOP 1 0 +OK Top of message follows From: "Nathan V. Patwardhan" <nvp> Subject: pop test Getting and Setting the Host and PortTheHost() function returns or sets the current
POP host. For example:
$obj->Host;returns the current POP host and sets the new POP host to new-pop.bar.com.
$new_host = 'new-pop.bar.com'; $obj->Host($new_host);The Port() function works like Host(),
returning or setting the current port the POP server is bound to:
$obj->Port;This returns the current port the POP server is bound to and sets the new port to 7000.
$new_port = 7000; $obj->Port($new_port); Retrieving the Message BodyNaturally, you'll want to read more than the headers of your mail messages, so Mail::POP3Client contains theBody(),
HeadAndBody(), and Retrieve() functions.
The Body() function outputs the body of the message,
and both HeadAndBody() and Retrieve()
output both the head and body of the message. Given the above
example, an entire message can be output with the following code:
#!/usr/local/bin/perl -w
use strict; # those who are late will not be served fruit cup.
use Mail::POP3Client;
my($pop, $num_mesg, $i);
$pop = Mail::POP3Client->new("nvp", # required
"xxxxxx", # required
"mail.somename.com", # pop host
110, # port (an integer)
0); # debug flag (an integer)
$num_mesg = $pop->Count;
print("You have ".$num_mesg." new message(s).\n");
for ($i = 1; $i <= $pop->Count; $i++) {
foreach ($pop->Retrieve($i)) { # show the message, $i
print $_,"\n"; # add a \n to end of each line
}
print "\n";
}
This example outputs the message:
~/ORA/PRK/Chapters/Mail/scripts> ./ex1_8.pl
You have 1 new message(s).
Received: (from nvp@localhost)
by mail.somename.com (8.8.5/8.8.5) id AAA03248
for nvp; Mon, 9 Jun 1997 00:11:09 -0500 (EST)
Date: Mon, 9 Jun 1997 00:11:09 -0500 (EST)
From: "Nathan V. Patwardhan" <nvp>
Message-Id: <199706090511.AAA03248@mail.somename.com>
To: nvp
Subject: pop test
Status: RO
this is a pop test
Deleting and Undeleting MessagesMessages can be deleted from the POP mailbox with theDelete() function. Delete temporarily
marks messages for deletion until the QUIT command
is received, at which time the messages are permanently removed.
Delete() takes one argument, the number of the
message to delete:
$pop->Delete(1);This deletes the first mail message.
Like most mail programs, Mail::POP3Client can undelete messages
marked for deletion before the program is terminated and the
changes saved. The
$pop->Reset(1); Reset() unmarks the first mail message from pending
deletion upon program termination.
Checking the ConnectionMost programs that require a user to log in will time out after a given period of time for security and resource reasons. Unattended programs are a welcome find to individuals wanting to cause harm to the system, and they are also taxing on the system, because the program is running idle. TheAlive() function
checks to see if the connection to the POP server is still open;
it returns true if the connection is good, or
false if the connection is closed. For example:
#!/usr/local/bin/perl -w
use strict; # be there or be square
use Mail::POP3Client;
my($pop, $is_alive);
$pop = Mail::POP3Client->new("nvp", # required
"xxxxx", # required
"mail.somename.com", # pop host
110, # port (an integer)
1); # debug flag (an integer)
$is_alive = $pop->Alive; # Alive() returns 1 if connected
if($is_alive == 1) {
print("You are currently connected to: ".$pop->Host."\n");
} else {
print("Error: disconnected!\n");
}
The above code causes the POP server to respond (if connected):
~/ORA/PRK/Chapters/Mail/scripts> ./ex1_10.pl POP3: POPStat at ./ex1_10.pl line 8 You are currently connected to: mail.somename.com Explicitly Opening and Closing ConnectionsPOP connections can be explicitly opened and closed withLogin() and Close().
Close() takes no arguments and closes the connection
to the POP server; here's a test using Close():
$pop->Close;
if($pop->Alive == 1) {
print "Connected.\n";
} else {
print "Not connected.\n";
}
Mail Helper ModulesTheMail helper modules covered in this chapter are
Mail::Folder, Mail::Internet, and Mail::Address. We call them
helper modules because they perform less complex tasks
than core Mail modules like Mail::Send or
Mail::POP3Client, yet come in quite handy when working with email messages.
Handle Folders with Mail::FolderOnce you've begun downloading and reading your email from a POP server, you might want to save or categorize your messages into folders which allow you to add, delete, save, and move messages easily.
Mail::Folder was written by Kevin Johnson
(kjj@pobox.com) as
an object-oriented, folder-independent interface to email folders
that supports
You'll need to create a Mail::Folder object before you can use
the module. Mail::Folder uses the
The options to the
Content-Length
DotLock
Flock
NFSLock
Timeout
DefaultFolderType
mbox format
with the following code:
$ftype = 'mbox'; $foldername = '/home/nvp/Mail/nvp'; $fldr_obj = new($ftype, $foldername [, %options]);Let's look at the key methods you'll need to get started with Mail::Folder.
$folder_obj->open($foldername); get_message()
$num = 24; $folder_obj->get_message($num); get_mime_message()
get_message_file()
$num = 2; $folder_obj->get_message_file($num); get_header()
$num = 45; $folder_obj->get_header($num); get_mime_header()
$num = 392; $folder_obj->get_mime_header($num); append_message()
$folder_obj->append_message($mi_ref); refile()
$folder_obj->refile($num, $fldr_ref); delete_message()
@all = (1, 2, 3, 5); $folder_obj->delete(@all); undelete_message()
@unmark(1, 3, 4, 6); $folder_obj->undelete_message(@unmark); message_list()
print $folder_obj->message_list."\n"; qty()
print "There are ".$folder_obj->qty." messages in your folder\n"; Handle Messages with Mail::InternetMail::Internet implements a number of helpful functions to manipulate a mail message, includingbody(),
print_header(),
and head().Mail::Internet is built on top of Mail::Header, which parses the header of an electronic mail message. Mail::Internet inherits the Mail::Header constructor style that requires a file descriptor or reference to an array be used. For example:
@lines = <STDIN>; $mi_obj = new Mail::Internet([@lines]);reads a mail message from STDIN (using a reference to an array). The following example reads a mail message from a filehandle, FILE.
open(FILE, "/home/nvp/Mail/nvp"); $mi_obj = new Mail::Internet(\*FILE); close(FILE);The print_header() function outputs the
header of a message to a file descriptor; the default
is STDOUT.
open(FILE, "/home/nvp/Mail/nvp"); $mi_obj = new Mail::Internet(\*FILE); close(FILE); $mi_obj->print_header(\*STDOUT);The above example outputs:
From nvp Mon Jun 9 00:11:10 1997
Received: (from nvp@localhost) by mail.somename.com (8.8/8.8) id
AAA03248 for nvp; Mon, 9 Jun 1997 00:11:09 -0500 (EST)
Date: Mon, 9 Jun 1997 00:11:09 -0500 (EST)
From: "Nathan V. Patwardhan" <nvp>
Message-Id: <199706090511.AAA03248@mail.somename.com>
To: nvp
Subject: pop test
X-Status:
X-Uid: 1
Status: RO
where print_body() also takes a file descriptor
as an argument, but only outputs the body of the message,
whereas the print() function outputs an entire
message.
Parse Email Addresses with Mail::AddressMail::Address was written by Graham Barr (gbarr@ti.com). This module parses RFC 822-compliant email addresses with the form:
"Full Name or Phrase" <username@host> (Comment Area)For example, under RFC 822, an address might be represented as:
"Nathan V. Patwardhan" <nvp@mail.somename.com> (No Comment)or
"Nathan V. Patwardhan" <nvp@mail.somename.com>The Mail::Address constructor parses an email address into three parts based on the categories shown above:
$addr = Mail::Address->new("Full Name or Phrase",
"username@host",
"(Comment Area)");
Mail::Address also outputs portions of the email address
with the functions phrase(), address(),
comment(), format(), name(),
host(), and user(). The phrase(),
address(), and comment() functions represent
the first, second, and third entities that were passed to the
Mail::Address constructor, where the phrase()
function when used as follows:
print $addr->phrase();outputs:
Nathan V. PatwardhanThe address() function when used like:
print $addr->address();outputs:
nvp@mail.somename.comAnd the comment() function when used like:
print $addr->comment();outputs:
No CommentA real electronic mail address can be "unmangled," or parsed from its user@somehost.com format, with the user()
and host() functions. The user() function
removes everything after and including @, and host()
removes everything before and including @. Using the previous
example of nvp@mail.somename.com, the following line:
print $addr->user;outputs:
nvpAnd the following line using the host() function:
print $addr->host;outputs:
nvp@mail.somename.com
If you create a Perl script called parsefrom.pl using
MIME::Head and Mail::Address to parse the From line
of an incoming mail message, and put a statement like:
|/path/parseform.pl your_usernamein your .forward file, your script passes the contents of the From line into Mail::Address, and parses a
mail address for each incoming mail message. Here's an idea of
how parseform.pl might be written:
#!/usr/local/bin/perl -w
use MIME::Head; # used to parse the message header
use Mail::Address; # used to parse the sender's address
use strict; # the hills are alive
my($header, $bar, $foo);
$header = new MIME::Head;
$header->read(\*STDIN); # read message from STDIN
$bar = $header->get('From', 0); # grab the header containing 'From'
# If the address doesn't match the form
# PHRASE ADDRESS (COMMENT), use the first regexp which won't
# try to match (COMMENT). Otherwise, grab everything.
if(....) {
#match address type 1
} else {
#match address type 2
}
Handling MIME DataNontext file types like executables, pictures, and file archives can be sent through electronic mail using MIME, or Multipurpose Internet Mail Extensions. MIME messages are sent in multiple parts separated by boundaries, with specific stamps representing the contents of the boundary. (MIME-encoded messages can also be referred to as multipart messages.)
Although many mail readers are able to parse and
decode the contents of MIME messages, others, like
For more information on sending mail with MIME attachments, see the discussion of Base64-encoding later in this chapter.
Format of the mailcap FileThe mailcap file is generally found in /etc or /usr/local/etc, but can be located anywhere, providing thatmetamail
knows where to find it. The MAILCAPS environment
variable can be set to point to the location of the
mailcap file if it is different than the default. mailcap entries are divided into four fields, separated by semicolons. The four fields are:
application/x-perl; ptkat %s; nonummyWith a description tag, the above entry could be illustrated like:
application/x-perl; ptkat %s; nonummy; \ description="pTk applications that should not be."The above entry represents a valid Content-type as specified in RFC 822, application/x-perl. When metamail encounters
the application/x-perl Content-type, ptkat
executes with one argument as well as the nonummy flag,
which tells ptkat not to suppress anything being sent to STDOUT.
mailcap entries preceded with a
Using the Mail::Cap ModuleMail::Cap follows the convention defined in RFC 1524, pointing to a default mailcap file or the one specified in the MAILCAPS environment variable. You can create a new Mail::Cap object with the default mailcap file as follows:
$mailcap = Mail::Cap->new();You can also create a new Mail::Cap object with a custom, or local, mailcap file. For example:
# use the mailcap file stored in /path called local_mailcap $my_mailcap_file = '/path/local_mailcap'; $mailcap = Mail::Cap->new($my_mailcap_file);Mail::Cap has implemented four functions to handle messages being analyzed by the mailcap file: view(),
compose(), edit(), and print().
Each function takes two arguments: a Content-type and the name of a
file to be operated on per the directives in the mailcap file.
The
Here's a simple program that takes a command-line argument and
handles the data from
#!/usr/local/bin/perl -w
use Mail::Cap;
$capfile = '/home/nvp/mailcap';
$mcap = Mail::Cap->new($capfile);
$mcap->view('image/jpg', $ARGV[0]);
This program spawns the external viewer corresponding to the
image/jpg Content-type, xloadimage.
It is extremely limited, however; if the file specified by
$ARGV[0] isn't of type image/jpg,
the program fails, and the file won't be displayed.
The mime.types FileMail readers aren't the only ones that handle MIME data. Web servers such as Apache have the responsibility of telling Web clients what kind of data they are sending, and for that they use an external file, generally called mime.types. The mime.types file lists Content-types with the file extensions associated with that type of file, such as .jpeg or .jpg for a JPEG image, or .html for an HTML file. An external file is used, because it's easier to add new entries to a file than recompile the program each time an entry is added.Like the mailcap file, the mime.types file contains a number of RFC 822-compliant Content-types, but the mime.types file generally doesn't contain information about external programs to be executed when a given Content-type is encountered; that decision is made on the browser's end. The mime.types file is used to create an association between a file's extension and Content-type. The server conveys the Content-type to the browser, and the browser takes it from there.
Here's a command-line-based program that parses a file's
extension (using File::Basename), and searches a mime.types
file for a matching extension. If an extension is found, the
Content-type of the document is returned and passed to the
Mail::Cap
#!/usr/local/bin/perl -w
use File::Basename; # for the file's extension
use Getopt::Long; # for the command-line arguments
use Mail::Cap; # to parse the mailcap file
# you're not leaving this table until you've eaten all of your peas
use strict;
# remember, we're using strict
my($infile, $capfile, $file_ext, $mimetype, $mailcap);
# program must be executed like: programname -file inputfile
# if not, output a usage message
GetOptions('file=s', \$infile);
$infile or usage();
# specific capfile. without declaring this, and creating an
# object like: $mailcap = new Mail::Cap, Mail::Cap will expect
# that you have an existing mailcap file (in /etc/mailcap or such),
# or have defined an environment variable MAILCAPS
$capfile = 'mailcap';
# From File::Basename, grab the extension of the file (field 2).
# This extension will be passed to lookup_type-by_extension, which
# returns a Content-type if the extension matches an entry in the
# mime.types file
($file_ext) = (fileparse($infile, '\..*'))[2];
$mailcap = Mail::Cap->new($capfile);
$mimetype = lookup_type_by_extension($file_ext);
$mailcap->view($mimetype, $infile);
sub lookup_type_by_extension {
my($ext) = @_;
my($ctype, @ftype, @found);
my($m_types) = 'mime.types';
open(TFILE, $m_types) or die("error opening $m_types: $!");
while(<TFILE>) {
# remove newlines
chop;
# get the content-type and extensions (file types) of
# mime.types entries by splitting each line of the file
# on the tab character
($ctype, @ftype) = split(/\t/, $_, 2);
# check to see if extension appears in @ftype array
# yes Virginia, I've been reading perlfaq4 :-)
my %found = ();
for(@ftype) { $found{$_} = 1; }
# if extension exists, there will be an element in @found,
# so scalar(@found) will be greater than zero. Return the
# content-type of the mime.types entry
return $ctype if $found{$ext};
}
close(TFILE);
}
sub usage {
print("Usage: $0 -file <filename>\n");
exit(0); # failure
}
The MIME ModulesThis section introduces you to the creation of, mailing, parsing, reading, and decoding multipart MIME messages using the MIME modules. We'll cover:
Encoding and Decoding with MIME::Base64As described previously, MIME makes it possible to send non-ASCII files via email. The way it performs this magic is via base64 encoding, which is then packed into a maximum of 76-character lines, an appropriate length for email. At the heart of the MIME modules is MIME::Base64, which implements theencode() and decode()
methods that handle streams of information to be base64-encoded
or -decoded. Here's what the base64 algorithm does:
#!/usr/local/bin/perl -w
### Script: test.b64
use MIME::Base64 ();
### clear text string
$to_encode = "There once was the good Duke of Earl,\n".
"who used to sit home and hack Perl.";
### output the clear text string
print("Clear String: \n". $to_encode ."\n");
### Now, encode the clear text string, $to_encode
$encoded = MIME::Base64::encode($to_encode);
### Let's output the encoded string
print("\nEncoded: \n$encoded\n");
### decode the clear text string
$decoded = MIME::Base64::decode($encoded);
### If all has gone okay, we're right back to where
### we started. Output the decoded string.
print("\nDecoded: \n".
$decoded
."\n");
Naturally, base64-encoded information can also be decoded
from an input file. Here's an example that does just that.
#!/usr/local/bin/perl -w
### Script name: test3.b64
use MIME::Base64 ();
$input = 'output.mime'; ### input file
$output_file = 'from_mime.gif'; ### output file
print("Base64-encoded file: $input\n");
print("Output image: $output_file\n");
open(INPUT, "$input") or die("Input file $input error: $!");
open(OUTPUT, ">$output_file") or die("Output file $output_file error: $!");
### Again, DON'T chop() lines from the input file. This will
### break the base64-encoding scheme for the input file and
### your output file will get corrupted!
while($line = <INPUT>) {
$decoded = MIME::Base64::decode($line); ### output decoded line
print OUTPUT $decoded;
}
close(OUTPUT);
close(INPUT);
Handling Multipart Mixed MessagesNow that you've been introduced to how information is base64-encoded, you might wonder why we've referred to MIME-encoded messages as multipart or mixed messages. The answer is that each document has a format or type that is considered when sending multipart messages. For example, you might send someone an electronic mail message containing ASCII text and a picture of yourself in GIF format. Since the mail message contains more than one type of content, it's referred to as containing multipart or multipart/mixed content.When you add content to a multipart message, you're asking your mail program to map a Content-type to the extension of the filename you've included. If your mail program can't determine the Content-type of one of the files you've attached, most programs will use a default Content-type. So, how's an ordinary mail message different from a multipart mail message? Normal mail headers contain the following information: From, Date, Subject, and To. If I were to send myself a plaintext (nonencoded) mail message, with the subject "Here's looking at you, Nate," the headers would look like:
From nvp Wed Mar 5 20:34:43 1997 Received: (from nvp@localhost) by mail.somename.com (8.7.5/8.7.3) id UAA00505 for nvp; Wed, 5 Mar 1997 20:34:42 -0500 (EST) Date: Wed, 5 Mar 1997 20:34:42 -0500 (EST) From: "Nathan V. Patwardhan" <nvp> Message-Id: <199703060134.UAA00505@mail.somename.com> To: nvp Subject: Here's looking at you, Nate Status: ROMultipart messages differentiate between content by using boundaries or dashed lines and character markers, which represent the type of content appearing between the boundaries. According to the RFC documents that deal with MIME messages, electronic mail messages that contain multiple types of content are bound like the following:
#!/usr/local/bin/perl -w
# we'll need this to check filenames
use File::Basename;
# needed for Base64 encoding
use MIME::Base64;
# the mail program we'll be using for sending
$mailprog = '/usr/lib/sendmail';
# the subject of our mail message
$subject = 'This is a test message in MIME format';
# who'll be receiving our mail message
$recip = 'nvp@ora.com';
# okay, let's generate a boundary for each section
# of the multipart mail message
$bndry = encode_boundary($subject);
open(MAILER, "|".$mailprog." -t ".$recip)
or die("error opening $mailprog: $!");
print MAILER "To: $recip\n";
print MAILER "X-Mailer: $0/1.0\n";
print MAILER "MIME-Version: 1.0\n";
print MAILER "Content-Type: multipart/mixed;".
"boundary=\"".$bndry."\"\n\n";
print MAILER "This is a multi-part message in MIME format.\n";
# now, let's do something with each @ARGV member if it's a file
# other entries will be treated as body text in the message
for(@ARGV) {
# if it's a file, let's encode the contents and
# send them to the mailer
if(-e $_) {
undef $/;
open(IN, $_) or die("no: $!"); $enc = <IN>; close(IN);
$encoded = MIME::Base64::encode($enc);
print $encoded."\n";
print MAILER<<TYPE;
$bndry
Content-Type: application/octet-stream; name="$_"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="$_"
$encoded
TYPE
} else {
# otherwise, we'll send the contents to
# the mailer as-is
print MAILER<<TEXT;
$bndry
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
$_
TEXT
}
}
# okay, we're done sending the parts.
# let's send the ending boundary
print MAILER $bndry.'--'."\n\n";
close(MAILER);
# subroutine to encode a boundary. There are *much* better ways
# to do this, but this subroutine is offered only for illustration
sub encode_boundary {
my $boundary = shift;
$boundary =~ s/(\s+)//g;
$boundary = '------------'.substr(pack('u', $boundary), 0, 24);
return $boundary;
}
# ok, if the entity name has an extension, we'll attach it as a file
# if it doesn't have an extension, we'll attach it as-is
sub do_part {
my $part = shift;
my($ext) = fileparse($part);
unless(defined($ext)) {
return undef;
} else {
return $ext;
}
}
Although functional, the above example is lengthy and painful in
the context of writing code from scratch versus using Perl modules.
Thankfully, there are a number of modules that simplify the process
of sending mail messages with attachments.
Create Simple Attachments with MIME::LiteSince the essence of this book has been to write applications with Perl modules instead of reinventing the wheel, the rest of this chapter focuses on some interesting uses of the MIME modules to generate and parse multipart messages. We'll start from the ground up, with MIME::Lite, developed by eryq@enteract.com.Let's start with MIME::Lite because of the lightweight nature of this module and straightforward API details that make it easy to get started writing applications with MIME::Lite. In fact, you can send email with an attachment using MIME::Lite in three steps:
Header Hacking with MIME::LiteSince you might choose to use MIME::Lite as part of an email application you're developing, you'll want the capability to add and remove items from the headers of messages you'll be sending. Let's review the contents of the object,$message, we've been using for the examples in
this section; $message contains:
$message = MIME::Lite->new(
From => 'nvp@somemailhost.com',
To => 'recipient@sybil-has-multiple-parts-too.com',
Subject => 'GIF and JPEG for you!',
Type => 'multipart/mixed');
Let's say you realized you wanted to CC this message to other
users, but you forgot to do so when you initialized
$message. MIME::Lite's add()
method allows you to add/modify members of the message
hash you initialized when you created the object, $message.
The add() method works like this:
$object->add(Header-Type => 'value');To add a CC header (for multiple recipients) to $message, type:
$message->add(CC => 'user@host1.com, user@host2.com');Bear in mind that hashes use a name/value pair that assigns one value to one name unless you're using hashes of hashes. This means you must be careful with the add()
method, as add()ing a header that already exists
means you'll overwrite the old value with the new. You might
opt to use the replace() method instead, where:
$object->replace(Header-Type => 'new-header-value');As you might have noticed, there are limitations to what you can do with MIME::Lite (it's light and fluffy, after all). MIME::Lite doesn't handle reading from filehandles, nor can it efficiently parse members of the message headers hash that was populated when you created a new MIME::Lite object. The remainder of this chapter discusses the modules distributed with the MIME-tools package. The MIME-tools package was also written by Eryq (eryq@enteract.com), and features a MIME header parser and multipart message generator similar to that in MIME::Lite. You'll want to use the MIME-tools package for any application you'll write that needs to both parse and create multipart MIME messages as well as manipulate mail header fields and their respective values.
More Robust Attachments with MIME::EntityThe MIME::Entity module works much like MIME::Lite. When you create a new MIME::Entity object, you're creating a hash of header/value pairs as you did in MIME::Lite. Instead of usingnew
for creating a new object, MIME::Entity uses the build
method that works in a similar fashion.
Remember the
Parsing MIME Messages with MIME::ParserIf you don't have a news or mail reader that supports MIME, you can save the MIME-encoded message as is and use MIME::Parser. With MIME::Parser, you create a new MIME::Parser object, a directory where both extracted content and prefixes for the extracted content are deposited. Then you tell MIME::Parser to read from a filehandle and handle the information accordingly. Let's examine the MIME::Parser's methods you'll need to parse multipart messages:
#!/usr/local/bin/perl -w
# for our MIME parsing needs
use MIME::Parser;
# for our filename/path parsing needs
use File::Basename;
# please use me - i like it!
use strict;
# where to put the decoded parts
my $output_path = '/tmp';
# derive the base filenames of extracted parts from
# the name of the script. uses basename method of
# File::Basename.yippee.
my ($parsed) = (basename($0))[0];
# our input file, a multipart message
my $input_file = '/tmp/mime.head';
my $parser = MIME::Parser->new();
# output directory for parsed files
$parser->output_dir($output_path);
# Basenames for parsed files
$parser->output_prefix($parsed);
# KEY PART: Now, write the entities (parsed from boundaries)
# to the filesystem
$parser->output_to_core();
# Parse the input file: (which can be @ARGV if you want it to be!)
open(INPUT, "$input_file") or die("Input error: $!");
my $entity = $parser->read(\*INPUT)
or die "couldn't parse MIME stream";
close(INPUT);
# Tell us about the MIME entities! You can suppress
# this if you don't want output for the sake of debugging
$entity->dump_skeleton; # for debugging
[1] Some SMTP servers do name lookups that disconnect requests at port 25 if the remote hostname can't be resolved. This practice is used to thwart evil commercial email programs that send junk mail by disguising themselves as fictitious hosts. |