So yesterday I had a go and wrote a little POP3 client using Net::POP3 and Mail::Internet (for message header parsing, to display From and Subject headers).
I'm sure that wheel's been invented hundreds of times before, but I didn't want it to do a lot; displaying an overview, reading messages, and deleting was enough.
(Maybe I could have used Mail::Box and friends since that suite of modules seems to do message parsing and POP3 as well, besides a host of other things. But it looked too complicated for a quick hack.)
Displaying the overview was the easiest since I just have to LIST to get a list of message IDs and message sizes in bytes. Then I can call TOP 2 on each message to get the headers and a tiny bit of the body and extract From and Subject from that.
Reading is a little more complicated. Messages are retrieved by message ID, but those IDs usually start counting at 1 and can be renumbered between sessions. But since HTTP is stateless, the script always closes the connection when it's finished. The next time it's called, the numbers might have changed. But as far as I understand it, UIDL is supposed to fix that -- call it on a message ID and the POP3 server gives you back a unique ID. So I made each subject a link back to the script but with parameters "action=read;which=<the UIDL>".
Then, since I read all the messages in at the start of the program, I just read the UIDLs as well. This gives me a hash reference ID => UIDL which I can invert to get a reverse lookup UIDL => MID. So then I look into this index to see whether a message with that UIDL exists. If so, I call GET on that message and just dump it straight to the screen (after putting <pre> around it and escaping & < > ").
Deleting was a little harder since, as I said, messages can be renumbered so I can't just pass in message IDs. But since I already had the code for finding a message based in UIDL, I just made it so that the check box said which message it was (MID) and also passed a hidden field for each message with the UIDL. (Looking back, it would have made more sense for the checkbox simply to have a value of the UIDL, but the way I have it now also works.)
Then when the script is called again, I see whether those messages exist, look up the MIDs and delete those messages. That way, I can delete those 80K spams I sometimes get from work and don't have to pull them over my ISDN connection.
So, not particularly elegant but it works. And since it's in a password-protected directory of my webserver, it should be moderately safe.