October 4th, 2008
Great! Now Fetch Me a Beer.
Eventually, I concluded that my recent experiment with switching to Mail.app was a failure. So, I installed a local copy of Mutt to read with, Fetchmail to keep my in-box populated, and Maildrop to help filter out the spam. Thanks to the magic of MacPorts, this was all fairly simple to do—however, there were a few subtle tricks to getting it set up, so I thought I’d record how I did it, in case anybody else might benefit from the effort.
For reference, as of this writing I’m using MacOS 10.4.11 (“Tiger”) and the Fetchmail 6.3.8 that comes pre-installed from Apple. Your mileage may vary if your configuration differs, though it is likely to work similarly with 10.5 (“Leopard”).
Contents:
- Installing Mutt and Maildrop
- Configuring Mutt and Maildrop
- Configuring Fetchmail
- Converting Mailboxes
Installing Mutt and Maildrop
I wanted a version of Mutt that could handle IMAP/SSL and POP3/SSL for incoming mail, and SMTP with STARTTLS for outgoing mail. Thanks to the lovely MacPorts ‘variants’ system, this was easy:
% sudo port install mutt-devel +imap +pop +smtp +sasl +ssl
This will pull in a few other packages, including expat, gdbm, gperf, and ncurses, but nothing that takes too long to build. You may wonder why I would mention build time—let me just say you should be glad you haven’t had to compile ghc lately.
Maildrop is the mail delivery agent from the Courier MTA, but it is also available as a stand-alone tool. It has a more graceful configuration syntax than my old standby Procmail, and since my filter setup is a lot simpler now than it used to be, I decided to make the switch. Once again, an easy install from MacPorts,
% sudo port install maildrop
This will pull in pcre, but I think that’s it. Some of the examples that follow assume that maildrop is installed as /opt/local/bin/maildrop, but you can edit the pathname accordingly as needed. You don’t need to install fetchmail, since it comes pre-installed as /usr/bin/fetchmail.
Configuring Mutt and Maildrop
After many years of corrupted mbox files, I decided now was the time to switch to using Maildir instead. The Maildrop package has a tool called maildirmake that makes it easy to get things set up, which I did as follows:
% maildirmake ~/Documents/Maildir
The common practise for Maildir is that nested folders are all stored at the top level of the directory, with names starting with a period (.) and nesting to be indicated by dots. However, as long as you are not trying to use your Maildir folder with Courier, it works just fine to create nested folders directly using maildirmake, e.g.,
% maildirmake ~/Documents/Maildir/Spam
% maildirmake ~/Documents/Maildir/Friends
Although nesting folders this way technically violates the Maildir spec, Mutt and Maildrop understand it just fine. Telling Mutt to use the new format required only a couple of lines in ~/.muttrc, namely:
set folder = "~/Documents/Maildir"
set mbox_type = Maildir
There are some other useful pointers for using Mutt with Maildir, but this is all that’s really necessary.
Configuring Fetchmail
Configuring Fetchmail to download my e-mail and to play nicely with the MacOS launchd tools was probably the hardest part of this process. Fortunately, it can be stripped down to a few simple instructions:
- Create a property list file for launchd. The easiest way to do this is to open the Property List Editor application,* but you can write it by hand, if you prefer. Mine looks like this:
< ?xml version="1.0" encoding="UTF-8"?> < !DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Disabled</key> <false /> <key>Label</key> <string>local.fetchmail</string> <key>ProgramArguments</key> <array> <string>/Users/michael/scripts/fm.sh</string> <string>--syslog</string> <string>--nodetach</string> </array> <key>RunAtLoad</key> <true /> <key>StartInterval</key> <integer>240</integer> </dict> </plist>
This configuration will tell the launchd tool to run fetchmail periodically to check for new mail. The Label field is a string that describes the job; I just chose local.fetchmail as being suitably descriptive. The StartInterval tells it how often to run—in my case, I have it running every 4 minutes (i.e., 240 seconds). The ProgramArguments array tells it what program to run, and what command-line arguments it should receive. The one that matters most here is --nodetach (or -N for short), which prevents fetchmail from going into the background as a daemon process. You can let it run that way, but it doesn’t play nicely with launchd.
Once you have this file filled in to suit your own desires, save it in ~/Library/LaunchAgents/local.fetchmail.plist. You may need to create the ~/Library/LaunchAgents/ directory, but there’s nothing special about it; just make a new folder in the Finder if it’s missing. Storing the property list here will permit launchd to find it when you log in. You can get more information about launchd from Apple’s web site, if you’re interested.
- Note:
- You could use cron instead of launchd if you prefer; I did not do so because cron jobs run all the time, and I wanted something that would run only when I’m actually logged in using the machine. If you want to use cron, I assume you’re probably savvy enough to figure out how to make it work.
- Install a wrapper for fetchmail. Notice that I am not running fetchmail directly, but instead am running a shell script named fm.sh. The reason for this is that when fetchmail discovers that you have no messages waiting, it exits with a status code of 1. By convention, any status code other than zero is interpreted as an “error” of some kind, and so launchd would get confused and think that fetchmail had failed. If it fails too many times, launchd gives up and stops running it. To work around this, my script just runs fetchmail, and if it returns status code 1, my script returns status code 0. This keeps launchd happy, without changing the behavior of the program. Here’s what that script looks like:
#!/bin/sh fetchmail $* fmstat=$? if [ $fmstat = 1 ] ; then exit 0 else if [ $fmstat = 75 ] ; then growlnotify --sticky --message "An e-mail was not filtered correctly." elif [ $fmstat != 0 ] ; then syslog -s -l NOTICE "fm.sh: fetchmail exited with status ($fmstat)" fi exit $fmstat fi # Here there be dragons
You will need to make sure the script is executable, and of course you should modify the path in the property list to point to wherever you actually keep it.
- Set up your fetchmail configuration. How to do this depends strongly upon where you get your mail from. The important points are that (a) The configuration should be stored in a text file named ~/.fetchmailrc, and (b) That file should not be readable or writable by anyone but you.
I generally fetch my e-mail from a handful of different IMAP/SSL servers, so my .fetchmailrc file looks similar to this (except, of course, that none of my login names or passwords is actually shown here in the example)
set invisible # don't modify message headers poll imap.domain1.com with protocol IMAP; user "login1" with password "secret1" is "yourname" here, and wants mda "/opt/local/bin/maildrop" nokeep nofetchall sslfingerprint "01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10" poll maildrop.domain2.org with protocol POP3; user "login1" with password "secret2" is "yourname" here, and wants mda "/opt/local/bin/maildrop" nokeep nofetchall sslproto ssl23 sslfingerprint "11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F:20" poll pop.gmail.com with protocol POP3; user "your.account@gmail.com" there with password "secret3" is "yourname" here, and wants mda "/opt/local/bin/maildrop" nokeep nofetchall ssl sslfingerprint "87:47:EA:7F:C6:B2:D1:DB:D3:64:4C:23:17:DB:E3:05"You will need to substitute in the appropriate host names, login names, and passwords. In the example, “yourname” means your user-name on the local machine, where you are planning to read mail once it’s delivered, while “login1” and “secret1” mean your user-name and secret password for your e-mail account on the remote server. The third example uses GMail, so in that case you could actually leave the host name alone, provided you’ve modified your GMail account settings to allow POP3 access. Check out the fetchmail(1) manual page if you have other needs.
The nokeep option tells fetchmail to delete the messages from the server once it is finished downloading; if you don’t like that, use keep in its place, which instructs it to leave your messages on the server—they will be skipped over for subsequent downloads. Using keep is probably a good idea while you are testing your configuration. The sslfingerprint options can be omitted, but I have included them to keep fetchmail from complaining about the self-signed server certificates many mail hosts use in lieu of a properly-signed certificate.**
- Set up your maildrop configuration. If you just want to drop mail into your default mailbox, this is easy: Just create a textfile named ~/.mailfilter that contains something similar to this:
DEFAULT=$HOME/Documents/MaildirReally and truly, that’s all you need; everything you don’t otherwise filter will be delivered to your default mailbox. My configuration is a tiny bit more complex, because I have a spam filter that runs on each incoming message, and messages it flags as spam are diverted to a separate mailbox. I also keep around a backup of the last 50 non-spam messages I received, in case I do something stupid and delete one—the recipe for that I lifted right out of the maildrop documentations. For reference, here’s how my configuration looks:
DEFAULT="$HOME/Documents/Maildir" PATH="$PATH:/path/to/my/programs" xfilter "mailtagger" # run the spam filter # Check against spam filter if ( /^X-MailTag: spam/:h ) to "$DEFAULT/Spam" # Keep a backup of the last several messages delivered. # Recipe taken from maildropex(7) manual page. cc "$DEFAULT/XBackup/" `cd "$DEFAULT/XBackup/new" && rm -f dummy \`ls -t | sed -e 1,50d\`` # Here there be dragons
At this point, you should be good to go. You can test your fetchmail configuration by running it a couple of times from the command line, e.g.,
% fetchmail -v -v --nodetach
Once you’re satisfied, it’s time to hand off the process to launchd, for which you should run:
% launchctl load ~/Library/LaunchAgents/local.fetchmail.plist
Modify accordingly if you saved your property list from Step (1) under a different name. At this point, Fetchmail should be fetching your e-mail from whatever servers you specified. I recommend you send yourself a couple of messages just to make sure it’s all right. Errors from fetchmail are written in the console log, so if you’re not getting what you expected, you should look there for feedback about why.
Converting Mailboxes
If all your mail resides on the mail server, you should be all set; however if you have any e-mail already saved locally under the control of Mail.app, you will have to do a bit of work to convert it.
Mail.app stores your mail in ~/Library/Mail/Mailboxes/, using a format similar to Maildir. If it were actually Maildir, you could use it directly, but it’s not quite the same. Each mailbox is stored in a directory named whatever.mbox, which contains an Info.plist that record some user preferences. However, instead of using three subdirectories (cur, new, and tmp) per mailbox, Mail.app uses only a single directory named Messages for each of its mail folders. Within Messages, each message is stored in a text file named something like nnnnnn.emlx, where nnnnnn is a sequence number of some kind; as far as I can tell, the numbers contain no information other than their identity.
The first line of an .emlx file contains a decimal integer, specifying the number n of bytes in the message. The next n bytes after that first line contain the message data itself, and the remainder of the file contains a short XML property list giving some attributes of the message. For my purposes, the easiest solution is to just discard the byte count and the property list entirely. The following Python code would suffice to convert a single .emlx file src and write the result into a new file dst:
def unpack_emlx(src, dst): with file(src, 'rb') as ifp: dlen = int(ifp.readline()) data = ifp.read(dlen) with file(dst, 'wb') as ofp: ofp.write(data)
To satisfy Maildir, you need simply extract each message into a uniquely-named file, and then move those extracted messages into your Maildir’s new folder. Once you have converted each of the .emlx files, the rest can be accomplished using mv. It is a tedious process, but not difficult. The unpack_emlx() function shown here is part of a somewhat longer script I wrote that applies it to each .emlx file found in a user-specified list of directories. I won’t post the code here, since it’s not really polished enough for others to use it, but if you really want a copy, drop me a line and I can file off some of its rough edges for you.
* This might not be available if you haven’t installed the developer tools, but on my system it can be found in
/Developer/Applications/Utilities.
** A rant about the foolish overcomplexity of the host certificate validation process is probably best saved for another day.
Filed by Michael at 17:21 under Technology, Tutorial
No Comments
4 Comments