This column is the result of DJ Adams' investigations into the Jabber technology and protocols. Learning about the Jabber technology and protocols, DJ immediately discovered that Jabber could lend itself to much more than just instant messaging.
|
Related Articles: The Jabber Jihad: Universal Instant Messaging Search O'Reilly Net for "jabber" |
In this article, I'd like to show you how easy it is to put together a simple system for pager-style notification of incoming mail, using some standard tools, Perl and, of course, Jabber.
The article is comprised of a number of sections:
I participate, OK, "lurk," in many mailing lists, so I've set up some filters that transfer list mail into appropriate mail folders. Mail coming from the London Perl Mongers list goes into my london.pm mail folder, mail from Squackers goes into my squackers folder, and so on. By the time the incoming mail has been analyzed and filtered into various mail folders, there's little left that makes it to my inbox. I classify my inbox mail as having higher priority, and I want to know when something has arrived there.
So, enter Jabber. I run a Jabber client on my desktop, and use it to communicate with colleagues at work. I decided that this would be a good place to receive notification of new mail in my inbox.
Jabber client communication is carried in containers called messages. There are a number of message types defined in the Jabber specification, but two in particular, "normal" and "chat," might lend themselves to delivery of such a mail notification. The "normal" type is akin to an e-mail message, and on clients that support it, arrives in your Jabber client "inbox." Figure 1 shows my Jabber client, WinJab, with a "normal" message in my inbox - a notification of approval of subscription request from Sabine - it has a subject ("Subscrip. Approved"), a date and time ("06/11/00 17:01"), and a body ("sabine@yak Approved your presence subscription request."), like an e-mail.
![]() |
Figure 1: WinJab showing a 'normal' message. |
The second type, "chat," is akin to an extended alert. When your Jabber client receives such a message, it opens a new window and displays the content of that chat message. In that new window, you then have the option of replying and holding a conversation with the initiator of the message, similar to DCC CHAT in IRC or AIM. Figure 2 shows such a popup window, this time from a different Jabber client called JabberIM.
![]() |
Figure 2: A popup window showing a chat conversation |
Since I don't want to be disturbed by windows popping up on my desktop too much, the "normal" type of message seems most appropriate for this type of incoming mail notification.
To filter and organize my incoming mail, I use procmail. Procmail is what receives my mail (from the MTA, sendmail) and I have a "recipe file" with rules about how to recognize, and what to do with, incoming mail.
The recipe file lives in my home directory and is called .procmailrc. It
contains sets of directives that are, in my case, a list of pattern matches
and mail folder destinations:
# Create backup of all mails :0 c Mail/backup # London PM :0 * ^To:.*london-list@happyfunball.pm.org Mail/london.pm # Squackers :0 * ^To:.*squackers@squack.com Mail/squackers |
| Figure 3: procmail recipes |
|
DJ Adams is a featured speaker at the upcoming O'Reilly Open Source Convention in San Diego, CA, July 23 - 27, 2001. Take this opportunity to rub elbows with open source leaders while relaxing in the beautiful setting of the beach-front Sheraton San Diego Hotel and Marina. For more information, visit our conference home page. You can register online. |
The first recipe is used to copy (c) each incoming mail into a backup
folder (Mail/backup) in case one of the later recipes loses it (I find it
easy to mess things up with procmail directives, so this is very useful).
The other two recipes shown are responsible for moving (no c) the mail into
a mail folder if one of the header lines matches a given string.
If the mail falls through all the recipes and hasn't been moved somewhere, then it ends up in my inbox.
As well as being able to move or copy mails into folders, procmail can also pass the mail (header and body -- the whole thing) to a program of your choosing. This effectively allows us to completely customize the handling for each or specific (matched on a pattern) mail, and is the key to being able to send a notification to my Jabber client.
By introducing a new recipe to my .procmailrc file, we can cause a program
to be started (and for this program to receive the contents of the mail)
for each mail that makes it through to my inbox:
# Send notification :0 c | ~/notify.pl |
| Figure 4: a procmail recipe to invoke a Perl script |
The pipe symbol ("|") in this recipe has effectively the same meaning as in the shell, in that procmail will pipe the mail message to the program you specify, which in this example is notify.pl in my home directory.
Now we have the framework for invoking a Perl script that will:
To parse the mail, we're going to use the Mail::Internet module, which
affords us a comfortable interface for getting at mail headers. To connect
to a Jabber server and send a message, we're going to use the Net::Jabber
module. So we should bring these modules in, along with a few other bits and
pieces that start off the script:
use strict; use Mail::Internet; use Net::Jabber; use constant RECIPIENT => 'dj@yak'; use constant SERVER => 'yak'; use constant PORT => 5222; use constant USER => 'notify'; use constant PASSWORD => 'notify'; use constant RESOURCE => 'perlscript'; |
Obviously this is only an example script and the values given
here are specific to my environment -- you should change them to
suit yours. Yak is the server where the Jabber server that I
use is running, and the server is listening on the standard Jabber
port 5222. I want the notification to go to me at the Jabber
address dj@yak. The other constants will be discussed later.
As mentioned above, procmail will pipe the mail to the script, which means that the mail's content, header and body is available on STDIN.
We're going to use the Mail::Internet module to save us from having to
parse and extract the mail header information ourselves. We'll reduce
the usage of Mail::Internet to a couple of lines, so as not to
overpower the rest of the script:
my $header = Mail::Internet->new(*STDIN)->head()->header_hashref();
chomp $header->{$_}[0] foreach keys(%{$header});
|
We start by creating a new Mail::Internet object, specifying that the
mail that should be parsed is coming in through STDIN, i.e., what's
piped to the script in the procmail recipe.
:0 c | ~/notify.pl |
Straight away we call the head() method on the newly created object,
which returns us a Mail::Header object, on which we call the
header_hashref() method, which returns us (surprise, surprise) a reference
to a hash, containing the mail header details.
The second line is to remove the trailing newlines from each of the
header values. Note that the actual values of the header hash are contained
in anonymous arrays (for example, to deal with multiple To: recipients),
so we have to refer to what is in our circumstances the first and only
value in these arrays -- hence the array index [0] in $header->{$_}[0].
At this stage, we have a hashref that looks like this:
{
'From' => [ 'sabine@yak.local.net' ],
'Date' => [ 'Sat, 4 Nov 2000 14:51:45 GMT' ],
'Subject' => [ 'Shopping list for this afternoon' ],
...
}
Now that we have enough information from the mail header to write a notification, we are in a position to connect to a Jabber server with a view to constructing and sending a message.
A Jabber message goes from person to person, or in our case, from program
to person. The significant point here is that both sides of the conversation
must be identifiable to Jabber, that is, have a Jabber ID (JID). My JID in
this example is dj@yak. Our notify program does not yet have a JID, but for
the sake of simplicity I'm going to use a JID that I've previously created
(using a normal Jabber client) and that I'll now use for the notify program.
The JID I created for the notify program was notify@yak, with a password
of notify.
We first make a connection to the Jabber server:
my $connection = Net::Jabber::Client->new();
$connection->Connect( "hostname" => SERVER,
"port" => PORT )
or die "Cannot connect ($!)\n";
|
Once we're connected, we must identify and authenticate ourselves. In this case, the script is going to identify itself as "notify" to the Jabber server on yak.
A Jabber
identification is actually more than just user@host -- to
support multiple connections as the same identity but from
different Jabber capable devices (imagine being connected
to your Jabber server on your laptop, your PDA, and your
workstation), a third qualifying part is used in the
identification -- the resource. I have chosen an arbitrary
word -- "perlscript" -- as the resource part of the identification
for this script.
So taking this script's identification values (from the
constants declarations above), the full identity is
notify@yak/perlscript
Some Jabber clients use their name as the resource part, so
that Sabine, using JabberIM as a client connected to the
Jabber server on yak, is identified as
sabine@yak/JabberIM
So, to identify and authenticate:
my @result = $connection->AuthSend( "username" => USER,
"password" => PASSWORD,
"resource" => RESOURCE );
if ($result[0] ne "ok") {
die "Ident/Auth with server failed: $result[0] - $result[1]\n";
}
|
Note that the AuthSend() method returns an array of potential
results -- the first line contains a general success/failure
flag and the second contains more detail.
Now it's time to create our message. We have already decided to
use a "normal" type message for our notification. We use a Net::Jabber
class Net::Jabber::Message that represents a Jabber message, and
create the message thus:
my $message = Net::Jabber::Message->new();
$message->SetMessage( "to" => RECIPIENT,
"subject" => "Email from $header->{From}[0]",
"body" => join("\n", "Subject: $header->{Subject}[0]",
"Date: $header->{Date}[0]") );
|
Of course, you can set whatever you want for the subject and body of the
notification; here I'm just sending information from a couple of the headers
of the e-mail received, which I retrieved using the Mail::Internet and
Mail::Header modules earlier.
The message type, set with a "type" parameter to the
SetMessage method, defaults to "normal," so I've not specified it here.
|
Once we've created the message, we can send it. It is worth looking at
these two actions (create and send) separately, as it underlines the
nature of what we're doing. I used the word "container" earlier to describe
a Jabber message. What we've done here is to create a container, and
its contents, and now we're going to send that container. But the sending
of the container is not a method of the Net::Jabber::Message class -- it's
a method of the Net::Jabber::Client class -- already instantiated in our script
into the $connection scalar.
In other words, the message is just a parcel of data that is sent from our client to the Jabber server, thus:
$connection->Send($message); |
Once we've sent the message, we're done, so we can use the Disconnect()
method to close the connection to the server; this is essentially a wrapper
around the Disconnect() method of the XML::Stream class,
which is responsible
for sending the proper closing XML tags to end the XML stream (the Jabber
XML is sent in "stream" mode) and closing the socket.
$connection->Disconnect(); exit; |
OK. As an overview, here's the whole script, with comments, so you can see it all at once and understand how it fits together:
# notify.pl
# Version 1
# E-mail notification script to Jabber client
use strict;
use Mail::Internet;
use Net::Jabber;
# Declare our constants
# ---------------------
use constant RECIPIENT => 'dj@yak'; # Jabber ID to be notified
use constant SERVER => 'yak'; # Jabber server to connect to
use constant PORT => 5222; # Port to connect to
use constant USER => 'notify'; # user this script connects as
use constant PASSWORD => 'notify'; # password associated with USER
use constant RESOURCE => 'perlscript';
# Read and parse email and extract header info
# --------------------------------------------
my $header = Mail::Internet->new(*STDIN)->head()->header_hashref();
chomp $header->{$_}[0] foreach keys(%{$header});
# Create a new Jabber client and connect
# --------------------------------------
my $connection = Net::Jabber::Client->new();
$connection->Connect( "hostname" => SERVER,
"port" => PORT )
or die "Cannot connect ($!)\n";
# Identify and authenticate with the server
# -----------------------------------------
my @result = $connection->AuthSend( "username" => USER,
"password" => PASSWORD,
"resource" => RESOURCE );
if ($result[0] ne "ok") {
die "Ident/Auth with server failed: $result[0] - $result[1]\n";
}
# Create a message and build it up
# --------------------------------
my $msg = Net::Jabber::Message->new();
$msg->SetMessage( "to" => RECIPIENT,
"subject" => "Email from $header->{From}[0]",
"body" => join("\n", "Subject: $header->{Subject}[0]",
"Date: $header->{Date}[0]") );
# Send the message
# ----------------
$connection->Send($msg);
# Disconnect from the Jabber server
# ---------------------------------
$connection->Disconnect();
exit;
|
Amazingly enough, it works -- here's a screenshot of a notification sent to me by the script:
![]() |
Figure 5: A notification arrives in my WinJab client |
This is only an example, and as such has a lot of limitations. You might want to make the RECIPIENT value modifiable by, for example, using a command line switch so you could put something like the following into your procmail recipe:
:0 c | ~/notify.pl -n qmacro@jabber.org |
Also, the script is set to die if something goes wrong, so you're not necessarily going to find out very easily if something in the configuration (or with the Jabber server) is awry.
But by far the most interesting (and intentional) omission is this: What if the user to notify is not connected at the time the notification is sent? Or what if the user is connected but temporarily doesn't want to be disturbed by such notification messages?
If you're interested in knowing the answers to these questions, then you might find the next article, A More Sensitive Mail Notifier interesting.
DJ Adams is the author of O'Reilly's Programming Jabber book.
Discuss this article in the O'Reilly Network General Forum.
Return to OpenP2P.com.
Copyright © 2009 O'Reilly Media, Inc.