Using Procmail to Mark As Read

I've seen a couple of blog posts explaining how to mark Maildir messages as read with procmail. All of them point to a thread on the procmail mailing list as the source of this solution. However, all of them fail to make any kind of attempt at explaining whats going on. After some trial and error, I was able to learn what was happening. First the snippet of procmail code:

[sourcecode language="bash"]
:0
* conditions
{
foldername=whatever

:0c
.$foldername/ # stores in .$foldername/new/

:0
* LASTFOLDER ?? //[^/]+$
{ tail=$MATCH }

TRAP="mv $LASTFOLDER .$foldername/cur/$tail:2,S"

HOST
}
[/sourcecode]

OK, I'm some-what familiar with procmail, however this code was not very intuitive. Why does a copy of the message (:0c) get moved into $foldername? What is HOST and what does it do? I will attempt to explain:

* conditions
This can be whatever you want. In my case, my Blackberry forwards a copy of all sent email to my inbox so that I can store it in the sent folder for future reference. However, I don't want to see "new" messages in my sent folder. I want them to be moved there and marked as read. All messages coming from my Blackberry have a From address of [email protected], so my condition is ^From.*[email protected]. I make the assumption that I won't be emailing myself (I have a separate email account for that).

foldername=whatever
This is just storing the path to the folder we're going to work with. In my case, I'm using my sent folder: foldername=/home/raam/mail/sent

:0c
.$foldername/

This places a copy of the message in the /new directory. This confused me, since the point of this code was to mark messages as read. But I discovered this step was necessary for the next few lines to work.

:0
* LASTFOLDER ?? //[^/]+$
{ tail=$MATCH }

First, let's read what LASTFOLDER is according to the procmail documentation:

This variable is assigned to by procmail whenever it is delivering to a folder or program. It always contains the name of the last file (or program) procmail delivered to. If the last delivery was to several directory folders together then $LASTFOLDER will contain the hardlinked filenames as a space separated list.

OK, so in this case, LASTFOLDER would contain the full path to the email that was copied to $foldername in the previous step. I'm not entirely sure what the rest of the commands do, but they're necessary.

TRAP="mv $LASTFOLDER .$foldername/cur/$tail:2,S"
Deciphering the mv command is pretty simple. It moves the email that we copied to $foldername to the /cur/ directory and renames it to end with :2,S. Cool, that makes the email client see the message as read. But what is TRAP? Again, let's RTFM and find out what TRAP is used for:

When procmail terminates of its own accord and not because it received a signal, it will execute the contents of this variable. A copy of the mail can be read from stdin. Any output produced by this command will be appended to $LOGFILE. Possible uses for TRAP are: removal of temporary files, logging customised abstracts, etc.

Interesting. But the next line is even more interesting!

HOST
What on Earth could this do? Well to my surprise it is directly linked to TRAP. Since the contents of TRAP will be executed when procmail terminates, we need a way of terminating procmail (for this block anyway). We do this with HOST. Here is a good explanation from another mailing list thread:

Lacking an explicit "exit" command in procmailrc, "HOST" in procmail is
what "exit $EXITCODE" would be in a shell.

So there you have it. I had no idea there would be so much happening behind these few lines of procmail code, but I'm glad I took the time to learn!

Write a Comment

Comment

22 Comments

  1. I’m not entirely sure why TRAP and HOST are used. IMHO this should work quite as well:

    :0
    {
    :0 c
    .$folder/

    :0 hi
    * LASTFOLDER ?? //[^/]+$
    | mv $LASTFOLDER .$folder/cur/$MATCH:2,S
    }

    I put the above lines in .mark_as_read_procmailrc and the following in .procmailrc:

    :0
    * whatever
    {
    folder=wherever
    INCLUDERC=$HOME/.mark_read_procmailrc
    }

  2. Thanks Hop. For some reason with the first method posted procmail was continuing so the mail was ending up both in my sent items (I set folder to Sent) as well as my Inbox. Your method fixed that, cheers.

  3. While hop’s method does look a lot cleaner and is easier to understand, it will log the whole mv command to the procmail logfile, making it harder to read. The TRAP/HOST combo logs the original folder it was delivered to (with /new/ instead of /cur/ unfortunately, oh well).

  4. This is a useful recipe!
    One minor note which could save folks some time.

    The recipe makes two references to .$folder

    That’s fine if in your .procmailrc you use the style of folder name you would see your mail client. If you use the filename from the filesystem that includes a leading period, it gets a second period when the routine is run causing the mail to be filed in a new unintended folder. (at least that’s what I observed.)

    To prevent this, just use $folder instead of .$folder in both places in the recipe above.

    Thanks!
    Steve

  5. I had some problems with the above recipe for folder names with spaces in them. Here’s how I fixed it:

    :0
    {
    foldername=”.2009 Archive”

    :0c
    “$foldername/” # stores in $foldername/new/

    :0
    * LASTFOLDER ?? //[^/]+$
    { tail=$MATCH }

    TRAP=”mv ‘$LASTFOLDER’ ‘$foldername/cur/$tail:2,S'”

    HOST
    }

  6. Handy recipe, thanks! Also, here’s what’s happening in that mystery bit:

    LASTFOLDER ?? (match against LASTFOLDER variable)
    / (match the first forward-slash in the path)
    / (begin extraction to remember a portion of string for later use)
    [^/] (match a character that isn’t a forward slash)
    + (match one-or-more of previous ‘non-slash’ expression)
    $ (match end of line)

    { tail=$MATCH } (set ‘tail’ variable to segment matched above)

    So if the incoming message is saved in:
    .YourFolder/new/1234567890.123456_0.mail.server.com

    The expression above extracts:
    new/1234567890.123456_0.mail.server.com

    The final mv uses this to move the message from new into cur, appending :2,S to indicate the message has been read.

    • The expression extract only “1234567890.123456_0.mail.server.com” and not the ‘new/’ directory. it extract all the remaining characters after the last forward slash.

      Your mistake is that “/” doesn’t match the first forward-slash in the path. This “/” match the closer “/” from the end of the LASTFOLDER string. The rest of your explanation is right. 🙂

  7. Hi, I am trying to auto delete some junk mail and I added /dev/null as the directory. But I get this description:
    procmail: Unable to treat as directory “/dev/null”
    procmail: Assigning “LASTFOLDER=/dev/null”
    procmail: Opening “/dev/null”

    I’m just wondering if this is working as expected. I have another directory for some spam that has an actual path, but am hoping I ‘m just deleting this junk without any other action.
    Thanks
    Jeff

    • Hi Jefferis,

      I haven’t looked at the Procmail code in quite some time, so unfortunately I’m not sure how to help. Hopefully another reader can chime in and make a suggestion.

      Good luck!

  8. This recipe was inspiration for a recipe I use to save copies of messages in a monthly folder, and mark the copies read. If they aren’t marked read then Thunderbird’s notification is always popping up to announce them as new.
    Recipe:
    :0c
    {
    foldername=.copies.`date +%Y`.`date +%b`/
    :0c:
    $foldername

    :0
    * LASTFOLDER ?? //[^/]+$
    { tail=$MATCH }

    :0h:
    * ? /bin/mv $LASTFOLDER ${foldername}cur/${tail}:2,S
    /dev/null
    }

    Note I added a c flag to the opening rule because we want a copy in the month folder and a copy to be passed to later rules. The last rule to deliver to /dev/null runs the mv command, and avoids an extra copy of the message being delivered to $DEFAULT

  9. Do we need to use ALL this code just to mark it as read? I already move messages into a specific folder, so all I need is mark that folder as read. Is there a more compact script to do it? I have this:

    :0
    * !^To: .*info@mydomain*
    $HOME/Maildir/.Catchall/

    I want to mark those moved messages as Read.

    Thanks

  10. Hello from a fellow procmail learner, 7 years later!

    This can be simplified and clarified I believe. Here’s how I handle Junk Mail, which need not be reported by my MUA as unread — I hate watching those unread numbers mount all day, and get notifications, as if something important is going on:

    :0c:
    * ^Subject: ***SUSPECTED SPAM***
    .Junk/

    :0
    * LASTFOLDER ?? ^.Junk/new//.+$
    {
    TRAP = “mv ${LASTFOLDER} .Junk/cur/${MATCH}:2,S”
    }

    Note that ‘HOST’ is unnecessary; that only applies when you want to exit from a ‘c’ recipe; the final recipe benefits from the above ‘c’ recipe, but is not a ‘c’ recipe itself. The intermediate variable ‘tail’ is unnecessary as well. In your post, your braces are unbalanced and your quotes are curly. That’s corrected here.

    As far as the part you were unsure about, the weird ‘/’ is the regexp capture token; once encountered, everything after the token as assigned to MATCH for later use. In your case, the regexp is forced to find the final slash, and return everything after it, stripping off the folder name and thus returning just the filename. I made mine more explicit to prevent possible confusion with another recipe.

    • Daniel,

      Thanks so much for your helpful and informative comment! 🙂

      I haven’t been using Procmail for many years, but I may just be looking into it again as I’ve recently started exploring Emacs-as-an-OS; the thought of reading my email inside my text editor just seems genius! 😀