Executing JS Code when receiving an Email with a specific header set

I spent the last two days figuring out exactly how the Thunderbird new email notifications work. My goal was to listen in new Emails for a specific header and then run a specific task. Before I go into details a general note on this first. Please be careful when doing this and be even more careful when passing that header value to your code. The code processing the new mail is privileged and the header value should be treated as unsafe. In the best case this could cause a DoS type attack on your extension, in the worst case attackers could use it to control the user’s Thunderbird.

When I started on this I though it would be easy, a matter of hours. I started with a simple message filter listener, which is what I found on the internet first:

Components.utils.import("resource://gre/modules/mailServices.js");
Components.utils.import("resource://gre/modules/iteratorUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");

const CUSTOM_HEADER_NAME = "x-my-custom-header";

let listener = {
  QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIMsgFolderListener]),
  msgsClassified: function(aMsgs, aJunkProcessed, aTraitProcessed) {
    for each (let msg in fixIterator(aMsgs.enumerate(), Components.interfaces.nsIMsgDBHdr))) {
      if (msg.getProperty(CUSTOM_HEADER_NAME)) {
        // do something
      }
    }
  }
  // other nsIMsgFolderListener methods ommitted for brevity
});

function setupListener(listener) {
  const mfn = Components.interfaces.nsIMsgFolderNotificationService;
  MailServices.mfn.addListener(listener, mfn.msgsClassified);
}

The problem that took me a while to figure out is that the “do something” block is never called. Until I found the actual soltuion, I tried different things like using a pure nsIFolderListener or creating a temporary message filter that takes care. I was convinced that the header values were just not parsed into the nsIMsgDBHdr yet. The reason for this was a very misleading comment in the nsIMsgDBHdr interface. But without some extra code, x-my-custom-header is never added to the properties of the nsIMsgDBHdr. This is done on purpose to keep the database small.

To make it work, we just need to set an extra pref:

let customHeaders = Services.prefs.getCharPref("mailnews.customDBHeaders").split(" ");
if (customHeaders.indexOf(CUSTOM_HEADER_NAME) < 0) {
  customHeaders.push(CUSTOM_HEADER_NAME);
  Services.prefs.setCharPref("mailnews.customDBHeaders", customHeaders.join(" ").trim());
}

I hope this saves you a few hours of digging. I certainly would have hoped to find a guide like this, but then again maybe I was looking at the wrong place with the wrong terms.