Check Exchange from the Command Line
Through the years, you tend to accumulate a suite of tools, practices and settings as you use Linux. In my case, this has meant a Mutt configuration that fits me like a tailored suit and a screen session that at home reconnects me to my IRC session and at work provides me with quick access to e-mail with notifications along the bottom of the terminal for Nagios alerts and incoming e-mail. I've written about all of these different tools through years, but in this article, I talk about how I adapted when one of my scripts no longer worked.
My e-mail notification script is relatively straightforward. I configure
fetchmail on my local machine, but instead of actually grabbing e-mail, I
just run fetchmail -c
, which returns each mailbox along with how many
messages are unseen. I parse that, and if I have any unread mail, I display
it in the notification area in screen. I wrote about that in my
February 2011 Hack and / column "Status Messages in Screen",
and up until
now, it has worked well for
me. Whenever I set up my computer for a new job, I just configure fetchmail
and reuse the same script.
Recently, however, we switched our mail servers at work to a central Exchange setup, which by itself wouldn't be too much of an issue—in the past I just configured Mutt and fetchmail to treat it like any other IMAP host—but in this case, the Exchange server was configured with security in mind. So in addition to using IMAPS, each client was given a client certificate to present to the server during authentication. Mutt was able to handle this just fine with a few configuration tweaks, but fetchmail didn't fare so well. It turns out that fetchmail has what some would call a configuration quirk and others would call a bug. When you configure fetchmail to use a client certificate, it overrides whatever user name you have configured in favor of the user specified inside the client certificate. In my case, the two didn't match, so fetchmail wasn't able to log in to the Exchange server, and I no longer got new mail notifications inside my screen session.
I put up with this for a week or so, until I realized I really missed
knowing when I had new e-mail while I was working. I decided there must be
some other way to get a count of unread messages from the command line, so I
started doing research. In the end, what worked for me was to use OpenSSL's
s_client
mode to handle the SSL session between me and the Exchange server
(including the client certificate), and then once that session was
established, I was able to send raw IMAP commands to authenticate and then
check for unread messages.
The first step was to set up an OpenSSL s_client
connection. Most people
probably interact with OpenSSL on the command line only when they need to
generate new self-signed certificates or read data from inside a
certificate, but the tool also provides an s_client
mode that you can use
to troubleshoot SSL-enabled services like HTTPS. With s_client
, you initiate
an SSL connection and after it outputs relevant information about that SSL
connection, you are presented with a prompt just as though you used Telnet
or Netcat to connect to a remote port. From there, you can type in raw HTTP,
SMTP or IMAP commands depending on your service.
The syntax for s_client
is relatively straightforward, and here is how I
connected to my Exchange server over IMAPS:
$ openssl s_client -cert /home/kyle/.mutt/imaps_cert.pem
↪-crlf -connect imaps.example.com:993
The -cert
argument takes a full path to my client certificate file, which I
store with the rest of my Mutt configuration. The
-crlf
option makes sure
that I send the right line feed characters each time I press
enter—important for some touchy IMAPS servers. Finally the
-connect
argument lets me specify the hostname and port to which to connect.
Once you connect, you will see a lot of SSL output, including the certificate the server presents, and finally, you will see a prompt like the following:
* OK The Microsoft Exchange IMAP4 service is ready.
From here, you use the tag login
IMAP command followed by your user name and
password to log in, and you should get back some sort of confirmation if
login succeeded:
tag login kyle.rankin supersecretpassword
tag OK LOGIN completed.
Now that you're logged in, you can send whatever other IMAP commands you want,
including some that would show you a list of mailboxes, e-mail headers or
even the full contents of messages. In my case though, I just want to see
the number of unseen messages in my INBOX, so I use the
tag STATUS
command followed by the mailbox and then (UNSEEN)
to tell it to return the
number of unseen messages:
tag STATUS INBOX (UNSEEN)
* STATUS INBOX (UNSEEN 1)
tag OK STATUS completed.
In this example, I have one unread message in my INBOX. Now that I have that
information, I can type tag LOGOUT
to log out.
expect
Now this is great, except I'm not going to go through all of those steps
every time I want to check for new mail. What I need to do is automate
this. Unfortunately, my attempts just to pass the commands I wanted as input
didn't work so well, because I needed to pause between commands for the remote
server to accept the previous command. When you are in a situation like
this, a tool like expect
is one of the common ways to handle it.
expect
allows you to construct incredibly complicated programs that look for
certain output and then send your input. In my case, I just needed a few
simple commands: 1) confirm Exchange was ready; 2) send my login; 3) once
I was authenticated, send the tag STATUS
command; 4) then finally log out. The
expect
script turned into the following:
set timeout 10
spawn openssl s_client -cert /home/kyle/.mutt/imaps_cert.pem
↪-crlf -connect imaps.example.com:993
expect "* OK"
send "tag login kyle.rankin supersecretpassword\n"
expect "tag OK LOGIN completed."
sleep 1
send "tag STATUS INBOX (UNSEEN)\n"
expect "tag OK"
send "tag LOGOUT\n"
I saved that to a local file (and made sure only my user could read it) and
then called it as the sole argument to expect
:
$ expect .imapsexpectscript
Of course, since this script runs through the whole IMAPS session, it also
outputs my authentication information to the screen. I need only the INBOX
status output anyway, so I just grep
for that:
$ expect ~/.imapsexpectscript | egrep '\(UNSEEN [0-9]'
* STATUS INBOX (UNSEEN 1)
For my screen session, I just want the name of the mailbox and the number
of read messages (and no output if there are no unread messages), so I
modify my egrep
slightly and pipe the whole thing to a quick Perl
one-liner to strip output I don't want. The final script looks like this:
#!/bin/bash
MAILCOUNT=`expect ~/.imapsexpectscript | egrep '\(UNSEEN [1-9]'
↪| perl -pe 's/.*STATUS \w+.*?(\d+)\).*?$/$1/'`
if [ "$MAILCOUNT" != "" ]; then
echo INBOX:${MAILCOUNT}
fi
Now I can just update my .screenrc to load the output of that script into one of my backtick fields instead of fetchmail (more on that in my previous column about screen), and I'm back in business.