View Message

A blog post describing all these techniques, with a demo extension, can be found at http://blog.xulforum.org/index.php?post/2011/03/14/Basic-MimeMessage-demo . The demo extension demonstrates listing attachments, reading them, and accessing various properties.

List Attachments

This is a quick recipe for getting a list of attachments present in a MIME message. This uses the new MimeMessage representation for multipart email messages. Technically speaking, the code belongs to Gloda, but it doesn't use the global database. It simply is a convenience layer on top of libmime that allows you to get a hierarchical representation of an email, manipulate it, examinate it, without having to abide by the streaming pattern enforced by libmime.

If you have a nsIMsgDbHdr, you should first obtain a MimeMessage by calling MsgHdrToMimeMessage first. The function is defined here and you have all the documentation written. Examples can be found by a mxr search.

This function returns a list of MimeMessageAttachments. This class is defined in http://mxr.mozilla.org/comm-central/source/mailnews/db/gloda/modules/mimemsg.js, some of the useful attributes of a MimeMessageAttachment are: contentType, name (the filename of the attachment). Starting with Thunderbird 3.3, there's also a size property. use nsIMessenger.formatFileSize to get a nice human-readable size.

/**
 * Recursively walk down a MimeMessage to find something that looks like an
 * attachment.
 * @param {MimeMessage} aMsg The MimeMessage to examine
 * @return {MimeMessageAttachment list} All the "real" attachments that have
 *  been found */
function MimeMessageGetAttachments(aMsg) {
  let attachments = aMsg.allAttachments;
  /* This first step filters out "Part 1.2"-like attachments. */
  attachments = attachments.filter(function (x) x.isRealAttachment);
  return attachments;
}

You can then stream the URL to whatever you want. You can even open the content in a new tab using Content Tabs

if (attachment.contentType.indexOf("image/") === 0) {
  //... create a <img> element
  imgElt.setAttribute("src", attachment.url);
}

Please note that in Thunderbird 3.3, MimeMessages have a new allUserAttachments property. This property lists all the attachments of the message as they would appear in the message pane. For instance, allAttachments will "see through" attached messages, and just return the attached message's own attachments. Conversely, allUserAttachments will treat attached messages as attachments.

Read Attachment

Continuing the previous example, here's how to access the contents of an attachment. This assumes the attachment contains text. If you need a raw stream of bytes, this is discussed later on. To check that the attachment is actually text, you can use a test such as if (attachment.contentType.match(/^image\//)). The UTF8 decoding may fail, or not, I don't really know about that, sorry.

This example assumes you're using Thunderbird 3.3, based on Gecko 2.0. Gecko 2.0 has nice Javascript modules (Services.jsm and NetUtil.jsm mainly), that simplify the code greatly.

// This is Thunderbird 3.3 only! Just replace the functions you miss with their definitions if you plan on being compatible with 3.1
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");

let url = Services.io.newURI(attachment.url, null, null);
let channel = Services.io.newChannelFromURI(url);
let chunks = [];

// Everyone knows that nsICharsetConverterManager and nsIUnicodeDecoder
//  are not to be used from scriptable code, right? And the error you'll
//  get if you try to do so is really meaningful, and that you'll have no
//  trouble figuring out where the error comes from...
let unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                       .createInstance(Ci.nsIScriptableUnicodeConverter);
unicodeConverter.charset = "UTF-8";
let listener = {
  onStartRequest: function (/* nsIRequest */ aRequest, /* nsISupports */ aContext) {
  },

  onStopRequest: function (/* nsIRequest */ aRequest, /* nsISupports */ aContext, /* int */ aStatusCode) {
    let data = chunks.join("");
    dump("Raw contents for the first attachment: \n"+data+"\n");
  },

  onDataAvailable: function (/* nsIRequest */ aRequest, /* nsISupports */ aContext,
      /* nsIInputStream */ aStream, /* int */ aOffset, /* int */ aCount) {
    // Fortunately, we have in Gecko 2.0 a nice wrapper
    let data = NetUtil.readInputStreamToString(aStream, aCount);

    // Now each character of the string is actually to be understood as a byte
    //  of a UTF-8 string. So charCodeAt is what we want here...
    let array = [];
    for (let i = 0; i < data.length; ++i)
      array[i] = data.charCodeAt(i);
    // Yay, good to go!
    chunks.push(unicodeConverter.convertFromByteArray(array, array.length));
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIStreamListener, Ci.nsIRequestObserver])
};
channel.asyncOpen(listener, null);

So I assume here that attachment is just an attachment of the MimeMessage you just streamed. What we're about to do now is stream the attachment contents. For that we need four things:

  1. a nsIURI that corresponds to the attachment
  2. a channel for that URI that we can open to stream its contents
  3. a listener that will be notified with the contents of the channel as they go: it needs to implement nsIRequestObserver and nsIStreamListener
  4. a unicode converter that will convert the stream of raw bytes into a string that's usable for us.

Item #1 is the url variable, and this is what allows us to obtain item #2, the channel variable. Item #3 is listener, and we just instanciate item #4 as unicodeConverter. What happens now is we start streaming the contents of the channel to listener asynchronously (hence the call to asyncOpen). Each time there's data available, we convert it to UTF8, and add it to the chunks array. Finally, when everything's been streamed, we put the pieces together, and stuff with them.

Reading raw bytes

If your attachment contains raw bytes, your best bet is to *not* run the UTF8 decoder of course. Then, you might want to check out the File I/O page, section "Write a binary file".

Save Attachment

You can also save attachments. Assuming you have msgHdrViewOverlay in scope, the following should allow you to trigger the "open attachment" or "save attachment" feature. Assuming att is a MimeMessageAttachment:

let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
let neckoURL = null;
neckoURL = ioService.newURI(att.url, null, null);
neckoURL.QueryInterface(Ci.nsIMsgMessageUrl);
let uri = neckoURL.uri;

let attInfo = new createNewAttachmentInfo(
  att.contentType, att.url, att.name, uri, att.isExternal
);

Then, HandleMultipleAttachments is your friend. Example:

HandleMultipleAttachments([attInfo], "open");
HandleMultipleAttachments([attInfo], "save");

You can even iterate over all the attachments, put them in an Array and then call HandleMultipleAttachments on the whole set of attachments. The available actions are here: msgHdrViewOverlay.js

Another useful action is saveAttachment(attInfo): it opens the regular dialog box to save an attachment to a file, and allows you to choose the name of the file you want to save.

Note: this API has changed starting from Thunderbird 6.0. You should use new attachmentInfo() and then attInfo.save() and attInfo.open(). See https://github.com/protz/GMail-Conversation-View/blob/cf7a372eafa16d61ffb02ef9494072e2ec7d7a65/modules/message.js#L678 for an example on how to handle both ways.

Access Message

  • gMessageDisplay.displayedMessage: An nsIMsgDBHdr (or dummy that's trying to pretend to be one, in the case of an .eml file), or null if no message is being displayed.

 

Get Message Body by Header

function getMessageBody(aMessageHeader)
{
  let messenger = Components.classes["@mozilla.org/messenger;1"]
                            .createInstance(Components.interfaces.nsIMessenger);
  let listener = Components.classes["@mozilla.org/network/sync-stream-listener;1"]
                           .createInstance(Components.interfaces.nsISyncStreamListener);
  let uri = aMessageHeader.folder.getUriForMsg(aMessageHeader);
  messenger.messageServiceFromURI(uri)
           .streamMessage(uri, listener, null, null, false, "");
  let folder = aMessageHeader.folder;
  return folder.getMsgTextFromStream(listener.inputStream,
                                     aMessageHeader.Charset,
                                     65536,
                                     32768,
                                     false,
                                     true,
                                     { });
}

Get Message Header by URI/URL

The easiest way to do this, which should work for all kinds of messages (messages in folders, attached messages, and .eml files), is the following:

msgHdr = messenger.msgHdrFromURI(messageURI);

If that doesn't work, there are some other options. This should work for URIs beginning with mailbox-message://, imap-message:// or news-message://.

msgHdr = messenger.messageServiceFromURI(messageURI).messageURIToMsgHdr(messageURI);

If your URL is an nsIURI object, and url.spec begins with mailbox://, imap:// or news://, you probably need this.

msgHdr = url.QueryInterface(Components.interfaces.nsIMsgMessageUrl).messageHeader;

If your URL is a string that begins with mailbox://, imap:// or news://, you should first convert it to an nsIURI object.

// the IO service
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                          .getService(Components.interfaces.nsIIOService);

// create an nsIURI
var url = ioService.newURI(urlString, null, null)

After that, you can use the method above to get the message header.

Get MIME Headers for any message

A Thunderbird contributor has written a function that gets the MIME headers of any message. The function is stored in github:

https://github.com/protz/thunderbird-stdlib/blob/master/msgHdrUtils.js#L428

As of Thunderbird 10, there is a new function that allows one to quickly grab the headers part of a given message body. If that function exists, this routine uses it. Otherwise it falls back on a less efficient method that streams the entire message (and is compatible back to Thunderbird 3).

Usage:

msgHdrGetHeaders(gFolderDisplay.selectedMessage, function (aHeaders) {
  if (aHeaders.has("reply-to"))
    dump("This message has a reply-to header; its value is "+aHeaders.get("reply-to")+"\n");
});

Please note that these are raw header values, so you might need to Cu.import("resource:///modules/gloda/utils.js"); and then run GlodaUtils.deMime on the value you obtained.

Search Messages

To do a search create an nsIMsgSearchSession (see nsIMsgSearchSession.idl).

  1. Create some search terms that define the search.
  2. Add the search scopes to the search session. This is the folders you want to search and how to search them (for example, for IMAP folders, specify whether the search should run on the server or run locally using the cached headers and message bodies).
  3. Register a listener to the searchSession so that you get notified of the search hits, and call the search() method on the searchSession.

There are a lot of examples in the code that show how to run searches. Here's a simple example from one of our unit tests: http://mxr.mozilla.org/comm-<wbr/>central/source/mailnews/base/<wbr/>test/unit/test_bug404489.js#<wbr/>183

You can define heterogeneous searches - that is, you can search over both IMAP and local folders in the same search. (The following backend implementation details probably don't interest extension developers.) The code that handles multiple search scopes / folders is in nsMsgSearchSession.cpp. For local searches (that is, local folders, or imap folders searched locally), we set up a repeating timer and in the timer callback, we do a little bit of the search and then wait for the next timer callback. See http://mxr.mozilla.org/comm-<wbr/>central/source/mailnews/base/<wbr/>search/src/nsMsgSearchSession.<wbr/>cpp#520 This avoids blocking the UI while doing the search. For IMAP searches, we run an IMAP URL and when we get the OnStopRunningUrl notification, we advance to the next folder / scope to search. For local searches, in nsMsgSearchSession::TimeSliceSerial, if we have finished the current folder we advance to the next folder in one of two ways:

Display a message yourself using iframes

Ok, these are very rough guidelines. First, you need an aspirin. Then, this diagram. Let's assume you have a nsIMsgDbHdr and you want to display the corresponding message somewhere.

You need to obtain a Necko URL from the nsIMsgDbHdr.

/**
 * Get a nsIURI from a nsIMsgDBHdr
 * @param {nsIMsgDbHdr} aMsgHdr The message header
 * @param {nsIMessenger} gMessenger The instance of @mozilla.org/messenger;1 you
 *  have created for your script.
 * @return {nsIURI}
 * */
function msgHdrToNeckoURL(aMsgHdr, gMessenger) {
  let uri = aMsgHdr.folder.getUriForMsg(aMsgHdr);
  let neckoURL = {};
  let msgService = gMessenger.messageServiceFromURI(uri);
  msgService.GetUrlForUri(uri, neckoURL, null);
  return neckoURL.value;
}

The gMessenger instance can be obtained using

const gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);

The next step is to create an iframe, catch some specific load events in sequence and set it to load the message URI properly. We assume you're adding the iframe into some <browser> element for instance, and we also assume that msgHdr is defined somewhere.

let iframe = YOURBROWSERHERE.contentDocument.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "iframe");
          iframe.setAttribute("type", "content");
          /* The xul:iframe automatically loads about:blank when it is added
           * into the tree. We need to wait for the document to be loaded before
           * doing things.
           *
           * Why do we do that ? Basically because we want the <xul:iframe> to
           * have a docShell and a webNavigation. If we don't do that, and we
           * set directly src="about:blank" in the XML above, sometimes we are
           * too fast and the docShell isn't ready by the time we get there. */
          iframe.addEventListener("load", function f_temp2(event) {
              iframe.removeEventListener("load", f_temp2, true);

              /* The second load event is triggered by loadURI with the URL
               * being the necko URL to the given message. */
              iframe.addEventListener("load", function f_temp1(event) {
                  iframe.removeEventListener("load", f_temp1, true);

                  /* The part below is all about quoting */
                  let iframeDoc = iframe.contentDocument;
                  //DO STUFF WITH THE MESSAGE CONTENTS HERE

                  /* Here ends the chain of event listeners, nothing happens
                   * after this. */
                }, true); /* end document.addEventListener */

              let url = msgHdrToNeckoURL(msgHdr, gMessenger);
              let cv = iframe.docShell.contentViewer;
              cv.QueryInterface(Ci.nsIMarkupDocumentViewer);
              cv.hintCharacterSet = "UTF-8";
              cv.hintCharacterSetSource = kCharsetFromMetaTag;
              iframe.docShell.appType = Components.interfaces.nsIDocShell.APP_TYPE_MAIL;
              iframe.webNavigation.loadURI(url.spec+"?header=quotebody", iframe.webNavigation.LOAD_FLAGS_IS_LINK, null, null, null);
            }, true); /* end document.addEventListener */

            YOURPARENTNODEHERE.appendChild(iframe);
          }
        }

What happens is that we first create the iframe. Next, we set its type to content, so that loading the message will automatically sanitize it (remove JS, block images) just like any regular message.

The tricky part is that as soon as you add the iframe into the DOM tree, it loads about:blank, so you need to make just you catch that event, and then you load the URI you want. Failing to do that will result in you seeing an incomplete iframe without the right properties on it, and you won't be able to load the next URI properly.

As soon as the first load event is fired, we get a URL from the nsIMsgDbHdr. Various magic is then required to make character encodings work properly (see http://mxr.mozilla.org/comm-central/...senger.cpp#609 for the gory details). Finally, the loadURI method is called.

The "?header=quotebody" parameter is there to get rid of the big header block and the attachments description (try without the extra parameter to see what I'm talking about). This is not the best way to do that, but I don't know of any other clean method to display only the message body, no inline attachments. The regular display code seems to use "?header=none". See http://mxr.mozilla.org/comm-central/source/mailnews/mime/src/nsStreamConverter.cpp#467 for the list of all possible values.

Calling the loadURI method triggers a second load event that we also catch. You can then do whatever you want with the message's body: get its inner HTML sanitized, modify the contents, add some fancy colors, just any regular HTML document.

The cool thing with this method is that is follows global preferences very closely (it sanitizes according to the user's policy, it respects the "display message body as" preference, etc.).

Tags (4)

Edit tags

Attachments (0)

 

Attach file