When fetchmail can't fetch mail, it's time to fall back to raw command-line commands.
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 in this column 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've written about that before in my February 2011 Hack and / column “Status Messages in Screen” (www.linuxjournal.com/article/10950), 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 I'm logged in, I can send whatever other IMAP commands I want, including some that would show me 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.
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.