BLURB Swallow is an open-source GUI MUA (mail user agent) for Unix written in Python. It uses Tkinter for its GUI, retrieves mail via POP, sends it via SMTP, and stores it in Maildir format. It is hacker-ware. WARNING: It is in an alpha state of reliability. The current version is available at: http://www.visi.com/~mdc/swallow/ VERSION 0.5.7 WARNING: Alpha code COPYRIGHT AND DISCLAIMER Swallow is Copyright (c) 2000-2003, Matthew Dixon Cowles . It is free software: it may be modified and redistributed, but only under the terms of the GNU General Public License, version 2 or (at your option) any later version. This program is distributed in the hope that it will be useful but WITHOUT ANY WARRANTY. Without even the implied warranties of MERCHANTABILITY and FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. CONTENTS Blurb Version Copyright and disclaimer Contents Requirements Optional modules Thanks Quickstart (Sorry, there's no other user documentation yet) More caveats Background and plans for the future Known and suspected bugs, bug reports To do To not do REQUIREMENTS Python 2.2 with the Tkinter and thread modules enabled (http://www.python.org) Pmw: Python megawidgets (http://pmw.sourceforge.net/) (Known to run with Pmw 0.8.4, 0.8.5, and 1.1) email module: Standard in Python 2.2, available for Python 2.0 and 2.1 from http://sourceforge.net/projects/mimelib/ ispell if you want the spelling checker to work (You probably already have it) OPTIONAL MODULES timeoutsocket.py (http://www.timo-tasi.org/python/timeoutsocket.py) (Known to work with version 1.12) If timeoutsocket is available, it's automatically used to handle socket timeouts better. THANKS To Guido, Tim Peters, Fredrik Lundh for Tkinter code and documentation, Greg McFarlane for Pmw, Harry George for the Usenet post that got me to start this project, Barry Warsaw for mimelib (now email), Oleg Broytmann for excellent examples of MIME decoding in Python, Remi Turk and Marc Jeurissen for excellent examples of MIME encoding in Python, Craig Durland for the paper that inspired the undo code, the PyChecker team for that great utility, Alex Martelli for wise news posts and good code to swipe, Tom Colburn for good coding advice, and to all the inhabitants of comp.lang.python. Thanks to Jamie Zawinski for his message-threading algorithm and for the idea of mail summary files, both of which I have unashamedly stolen. They're both described at: http://www.jwz.org/doc/ Thanks especially to the people who have reported bugs and sent patches: Tim Everett, Greg Green, Jason R. Mastaler, Brian Hoffman, and Albert Wagner. QUICKSTART Sorry, this and the comments in the example prefs files are all the documentation there is so far. Please let me know what parts of this are unclear and what else needs to be covered. Make sure that you have the items listed above in the Requirements section. Put swallow.py somewhere that you can execute it. Your home directory would be fine. Inspect the files minimal.swallowprefs.py and perhaps complete.swallowprefs.py. Create a file .swallowprefs.py in your home directory with the appropriate settings. The file must be writable only by you. If you store POP passwords in it, it must be readable only by you. It is loaded as though it were a Python module: that means that the configuration is very flexible. It also means that the configuration syntax may be annoying to folks who don't know or don't like Python. If you currently have your mail stored in a Maildir, back it up. There are surely plenty of bugs here. For all that, you should probably read only a copy of your incoming mail with this software. I'd hate for you to lose mail because of a bug. Run the program. It will create a Maildir for you if you don't have one and will create outbox and trash maildirs in the directory specified by userMailboxDirParent in the prefs file. The root window is an overview of your mailboxes. Double-clicking on a line will open a browser for the mail in that mailbox (pressing space or return will open a browser for the highlighted one). In an similar fashion, you in turn open an viewer (for most mail) or an editor (for mail in your outbox). Most of the controls should be obvious. Keystroke commands are (they're available in a window in the app too): In the overview of mailboxes (main window) m New mail up arrow Move selection up down arrow Move selection down return Open viewer for selected mailbox space Open viewer for selected mailbox click Select mailbox double-click Open viewer for selected mailbox q Quit program unless mail editors are open Q Quit program even if mail editors are open In a mailbox viewer up arrow Move selection up down arrow Move selection down left arrow Scroll left right arrow Scroll right return Open selected mail space Open selected mail click Select mail double-click Open selected mail s Save selected mail to folder(s) returned by getSaveFolders() in prefs, as long as the mail has been read S Save even if mail hasn't been read d Move selected mail to trash folder if read D Trash even if mail hasn't been read f Forward selected mail, decoding text parts and enclosing binary enclosures F Forward selected mail without doing any MIME decoding r Reply to selected mail R (note capital) Reply to all l Reply to selected mail's To address (good for mailing list replies) m New mail w Close window t Raise toplevel window Viewing mail f Forward current mail, decoding text parts and enclosing binary enclosures F Forward current mail without doing any MIME decoding r Reply R (note capital) Reply to all l Reply to mail's To address (good for mailing list replies) h Toggle display of all headers w Close window i Close window S (note capital) Save mail to file P (note capital) Print mail (see prefs) m New mail left arrow Previous mail right arrow Next mail up arrow Scroll up one line down arrow Scroll down one line u Scroll up one page - Scroll up one page space Scroll down one page / Search text | Pipe mail to external program specified in prefs t Raise toplevel window b Raise mailbox window Editing outbound mail control-l Scroll to put insertion point near center of the window tab Go to next field (there is no way to put a tab in mail) shift-tab Go to previous field control-z Undo control-y Redo control-up Scroll up control-down Scroll down (The standard emacs-like bindings from the Tkinter Text widget also work.) Other windows up arrow Scroll up one line down arrow Scroll down one line space Scroll down one page u Scroll up one page - Scroll up one page w Close window When forwarding mail, you generally want the ordinary forward ("Forward" from the buttons' menus and lowercase-f). It puts the text parts of the inbound mail in the outbound mail editor and attaches any enclosures that were in the inbound mail. The other sort of forwarding ("Forward verbatim" on the menus and uppercase-f) puts an exact copy of the inbound mail in the outbound editor. No enclosures are decoded or attached, their ugly encoded forms are there in the editor window. This sort of forward is useful for complaining about spam since it shows exactly what you received. Both sorts of reply put a MIME-decoded version of the inbound mail in the editor window like the ordinary forward function but they ignore non-text enclosures. The only difference between two sorts of reply are the address or addresses that are replied to. The ordinary sort of reply ("Reply" on the buttons' menus and lowercase-r) does what you'd expect: the default To address is the inbound mail's Reply-to or, if there isn't one, the From. Reply to all (uppercase-r) replies to all the addresses (but yours) in the From, To, CC, and Reply-to headers. Mailinglist reply (lowercase-l) replies to the inbound mail's To address. As the name implies, it's meant to be useful in following up to a mailinglist post. Naturally, you can change the addresses all you like once the editor is open. Default headers and text for forwarded messages and replies can be controlled by code in your preferences file. See the complete example preferences file. The same is true of default folders to save mail in. If you've had Swallow strip HTML tags in the mail viewer, they're also stripped in the default text of replies and forwarded messages. The overview does a reverse lookup on nicknames so From and To addresses will be shown as nicknames in the overview if they're in your addressbook. Things that are not obvious (by my standards of obviousness, anyway): Clicking on an item in the addressbook browser puts the address (and not the nickname) on the clipboard. So if you click on a line in the addressbook and then mouse-2 on one of the address lines in a mail composing window the address will be pasted there. Clicking on the last log line display in the overview opens the log viewer just as choosing Log from the Window menu does. If the display shows what it considers to be an error, it will turn red and won't show any more messages until you click on it. After a spelling check, words that ispell has suggestions for are highlighted in red by default. If ispell didn't have any suggestions, the word is highlighted in purple by default. Right-mousing on a word that ispell has suggestions for opens a window with ispell's suggestions in it. You can pick one of the suggestions or add the word to your personal dictionary. If you right-mouse on a word that ispell doesn't have suggestions for, you get a window that allows you to add it to your dictionary. Searching the text of mail that's open in a viewer is implemented in a slightly odd way (surprise!). If it turns out to be a bad idea, I'll change it. I am trying to avoid popping up a dialog for the search string and then making people lean on cokebottle-g to find what they want. This is not necessarily the perfect way to avoid that. You begin entering a string to search for by typing a slant. After the slant, text characters are added to the search string rather than being interpreted as commands (the arrow keys still work normally). Typing a return ends the search string. Instead of scrolling to your first match, all the text in the viewer window that matches the search string is highlighted. If there are more than 100 matches, Swallow only highlights the first 100 and shows "first 100 highlighted" instead of the number of matches. Typing another character or two will likely produce a more useful result. As it stands, you'll want to scroll around yourself to find what you were looking for. To unhighlight everything, search for nothing; i.e. type / return. Searches are case-insensitive. A more full-featured addressbook and a spam-tracing helper are included as optional parts. See README.swdb and README.spamgrep (respectively) for more information about them. MORE CAVEATS Swallow stores mail that you're viewing or editing (including MIME enclosures) in memory. An absurdly large mail could swamp VM and bring your machine to its knees and/or crash the Python interpreter. Swallow tries to prevent you from doing that by refusing (by default) to fetch mail larger than 20MB and asking before fetching mail larger than 10MB. Mail that large should be handled readily by any modern machine. You can override those limitations without difficulty but be careful before you open that mail in which your buddy sent you three movies and a CD. Storing mail in VM is a design limitation and is unlikely to go away in the foreseeable future. You can crash swallow without difficulty. You shouldn't be able to crash the underlying Python interpreter (unless you run it out of memory). But because the preferences file is loaded as Python code it is effectively part of the program. Syntax errors there will prevent the program from running. Putting values in it that are of the wrong type will most likely cause the routine that uses that value to crash. I could write a bunch of error-checking code and I probably will if crashes of this sort turn out to be a serious problem in practice. But Python's error messages are generally quite informative. Error-checking code that I wrote would probably just put about the same message up in a slightly different way. Most runtime error messages are shown in the status area above the overview and saved in the log. That's an experiment. It would be very easy for you to miss one and therefore be confused about why swallow isn't doing what you think it should. If that turns out to be a problem in practice, I'll probably have it put up a bunch of modal dialog boxes. But I honestly don't like them very much for a purpose like this one. BACKGROUND AND PLANS FOR THE FUTURE swallow was inspired by Harry George's post to comp.lang.python titled "PyBalsa?" and its various followups. Thanks. I wrote swallow for myself and it does things in a way that suits me. I freely confess to having hacked it up: much of the code ought to be cleaned up and I have not spent nearly enough time with RFCs 2045-9 to know that the MIME implementation here is adequate. Indeed, there are probably numerous RFC violations here. I'm usually the first person to criticize a coder for not studying and carefully following the RFCs. So please consider me already properly chastised and just tell me about my errors. I'd be very glad to hear feedback from anyone hackish or daft enough to use swallow. Please mail me at if you find bugs, have suggestions for improvements, or find it useful. If there's enough interest, I'll be glad to set up a mailing list. I plan to continue to develop swallow along the lines that its users request. If I'm the only user, I'll hack it according to my needs. If other folks request features, I'll put them in an informal database that I'll go to when I have time to work on it. Improvements from others will be accepted with gratitude though I can't promise to follow every suggestion or merge every patch. While the code is new, I intend to upload updated versions relatively often. KNOWN AND SUSPECTED BUGS, BUG REPORTS (Does not mean that there aren't plenty of other bugs.) The RFC822 address parser could probably be confused by seriously weird addresses. When you report a bug, it will help us both if you are as specific as possible about the conditions that caused it. A bug report that goes, "Sometimes it crashes when there is new mail" is of only a little help. Including at least Python's error description and traceback will make it much easier for me to find and fix the bug. Of course a patch that fixes the bug is best of all. TO DO Lots of code cleanup (still). PGP/GPG integration. Though I'm honestly not all that enthusiastic about it since I rarely need to send signed or encrypted mail. TO NOT DO This is a list of possible changes that I have considered but decided against implementing. The reason for my decision is given in each case. If you'd like to see one of these features, it will be fastest to implement it yourself. If you'd like to try to change my opinion, arguing against my rationale will be most useful. Good patches will be considered carefully. Modularize this mess: some or many of the classes here could more naturally be modules. Rationale: I want to distribute a minimum number of files. Jean-Claude Wippler (www.equi4.com) is right: installing software is a bore. GUI interface to config file Rationale: Python is more flexible. Configuration syntax other than Python Rationale: In my experience, the configuration syntax of highly-configurable software routinely forces the user to go through nutty contortions (often involving regular-expression syntax that's slightly incompatible with all other regular-expression syntax) to get the desired result. I think that that kind of syntax would often be simpler if it were a proper programming language to begin with. As far as I can tell, Python (at least the Python required for the configuration file) is no more difficult to figure out than mutt's configuration syntax (and I like mutt a lot). Mbox support (i.e. saving mail in one long file delimited by lines beginning "From ") Rationale: I don't like storing mail in mbox format. I think it's ugly and unreliable. Rewriting a few bytes in the middle of a ridiculously long file would be a big PITA. IMAP support Rationale: IMAP is good for manipulating mail on the server. But I'm a mail admin too and don't much want to give people reasons for leaving their mail on my server. As for sending passwords encrypted, APOP authentication seems to be just fine on that count. And besides, I don't know of very many mail admins whose servers support sending mail passwords encrypted since that means that they have to be in plain text on the server. Macintosh and/or Windows port Rationale: Maildirs require that hard links work (for our purposes here, essentially an atomic rename across directories). Perhaps there's a reasonable way to work around that in those OSes but doing so will require someone else's efforts. See above about good patches. Other than the requirement that Maildirs work, there isn't anything in the code that I know would cause a problem running under another OS. Hierarchical mailboxes Rationale: A flat structure seems sufficient. Modifier keys for keyboard control keystrokes (or, proper menus with underlines in them) Rationale: I came to this project from using mutt (a very fine MUA). It uses ordinary alpha keys (no control, alt, meta, shift, coke-bottle) for keyboard control and I find that I like that. The overview and mail viewer don't take keyboard input so there's no need to shift the keys. And underlines are very ugly. Stop arbitrarily changing the Tkinter look and feel Rationale: I don't much like the Tkinter look and feel. Maybe it's baby-duck syndrome (and maybe not, I'm a graphic designer when I have another hat on). It's far too blinky and bright. Buttons light up when you pass the mouse pointer over them -- would you not be able to tell they were buttons otherwise? The default colors are ugly and the way the keys work when editing text is silly. Automatically do something with MIME-encoded content (mailcap, etc) Rationale: Too many possible security problems. This mailer will happily save non-text MIME parts and then you can do whatever you like with them. But it will not automatically or quasi-automatically feed data from the network (i.e. mail) to other programs. Implement the SMTP AUTH command What it is: A way to login to an outbound mail server before using it to send mail, sort of the way you login to a POP server in order to fetch your mail. Why someone might want it: Users whose machines are off the mail server's local network but who want use it to get and send mail pose a certain difficulty. Postmasters commonly allow POP connections from remote machines but hardly anyone wants to send passwords in the clear across untrusted networks. APOP authentication can sometimes be a help but it requires that users' passwords be stored on the mail server in plain text and few postmasters are willing to do that. Sending mail is an even bigger problem: How is the server to tell a legitimate user from a spammer and let only the legitimate user relay through it? Various solutions have been used: POP before SMTP, SMTP AUTH, and so on. Rationale: SMTP AUTH with good security (CRAM-MD5 or better) does not appear to be widely implemented and seems to be kind of a mess to implement. (If you disagree about that last bit, please read RFCs 2554, 2595, 2222, 2195, and 2104 before telling me that I'm wrong.) And besides, there's a solution that's much simpler: using ssh. It's easy to use ssh to forward local ports to a remote machine: ssh -lmyusername -L25:mail.example.com:25 -L110:mail.example.com:110 mail.example.com That command, run as root, forwards the local POP and SMTP ports to the mail server over at example.com. You'd just list "localhost" as your mail server in .swallowprefs.py and the data would be forwarded automatically. You even get encryption for free. But what if (as is very likely) you don't have login privileges on the mail server? Doesn't matter. All you need is login privileges on a machine that's allowed to do POP and SMTP by the mail server: ssh -lmyusername -L25:mail.example.com:25 -L110:mail.example.com:110 myworkstation.example.com The mail server will see the requests come from myworkstation.example.com. You can even add a -C to request compression of the data stream. I believe that stunnel (www.stunnel.org) can be made to do the same thing but I've never used it since I already have ssh.