Bug 661410 - Implement UI for out-of-process plugins. r=Standard8
authorMike Conley <mconley@mozilla.com>
Mon, 26 Sep 2011 22:33:14 +0100
changeset 8836 9fa12b677b96af32705c70646afa5ce7fc871f17
parent 8835 ad1f76cfcbcc4022373b78129ae685ee2f363a10
child 8837 06fafe4cdac0de94841306f9c7f6baf7cd57fea8
push id177
push userbugzilla@standard8.plus.com
push dateTue, 27 Sep 2011 20:01:30 +0000
treeherdercomm-aurora@0a289bc5e5ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs661410
Bug 661410 - Implement UI for out-of-process plugins. r=Standard8
mail/app/profile/all-thunderbird.js
mail/base/content/mailWindowOverlay.xul
mail/base/content/msgMail3PaneWindow.js
mail/base/content/plugins.js
mail/base/content/specialTabs.js
mail/base/jar.mn
mail/locales/en-US/chrome/messenger/messenger.properties
mail/test/mozmill/content-tabs/html/blocklist.xml
mail/test/mozmill/content-tabs/html/blocklist_details.html
mail/test/mozmill/content-tabs/html/plugin.html
mail/test/mozmill/content-tabs/html/plugin_crashed_help.html
mail/test/mozmill/content-tabs/html/plugin_update.html
mail/test/mozmill/content-tabs/html/unknown-plugin.html
mail/test/mozmill/content-tabs/test-lwthemes.js
mail/test/mozmill/content-tabs/test-plugin-blocked.js
mail/test/mozmill/content-tabs/test-plugin-crashing.js
mail/test/mozmill/content-tabs/test-plugin-outdated.js
mail/test/mozmill/content-tabs/test-plugin-unknown.js
mail/test/mozmill/shared-modules/test-content-tab-helpers.js
--- a/mail/app/profile/all-thunderbird.js
+++ b/mail/app/profile/all-thunderbird.js
@@ -695,13 +695,32 @@ pref("dom.ipc.plugins.enabled", true);
 // OS calls in the plugin process, then arranging to make certain OS calls
 // in the browser process.  Eventually plugins will be required to use the
 // NPAPI to manipulate the cursor, and these workarounds will be removed.
 // See bug 621117.
 #ifdef XP_MACOSX
 pref("dom.ipc.plugins.nativeCursorSupport", true);
 #endif
 
+// plugin finder service url
+pref("pfs.datasource.url", "https://pfs.mozilla.org/plugins/PluginFinderService.
+php?mimetype=%PLUGIN_MIMETYPE%&appID=%APP_ID%&appVersion=%APP_VERSION%&clientOS=
+%CLIENT_OS%&chromeLocale=%CHROME_LOCALE%&appRelease=%APP_RELEASE%");
+
+// By default we show an infobar message when pages require plugins the user has
+// not installed, or are outdated.
+pref("plugins.hide_infobar_for_missing_plugin", false);
+pref("plugins.hide_infobar_for_outdated_plugin", false);
+
+#ifdef XP_MACOSX
+pref("plugins.use_layers", false);
+pref("plugins.hide_infobar_for_carbon_failure_plugin", false);
+#endif
+
+pref("plugins.update.url", "https://www.mozilla.com/%LOCALE%/plugincheck/");
+pref("plugins.update.notifyUser", false);
+pref("plugins.crash.supportUrl", "https://live.mozillamessaging.com/%APP%/plugin-crashed?locale=%LOCALE%&version=%VERSION%&os=%OS%&buildid=%APPBUILDID%");
+
 // Windows taskbar support
 #ifdef XP_WIN
 pref("mail.taskbar.lists.enabled", true);
 pref("mail.taskbar.lists.tasks.enabled", true);
 #endif
--- a/mail/base/content/mailWindowOverlay.xul
+++ b/mail/base/content/mailWindowOverlay.xul
@@ -77,16 +77,17 @@
 <script type="application/javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
 <script type="application/javascript" src="chrome://messenger/content/mailTabs.js"/>
 <script type="application/javascript" src="chrome://messenger/content/messageDisplay.js"/>
 <script type="application/javascript" src="chrome://messenger/content/folderDisplay.js"/>
 <script type="application/javascript" src="chrome://messenger-newsblog/content/newsblogOverlay.js"/>
 <script type="application/javascript" src="chrome://messenger/content/mail-offline.js"/>
 <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
 <script type="application/javascript" src="chrome://messenger/content/msgViewPickerOverlay.js"/>
+<script type="application/javascript" src="chrome://messenger/content/plugins.js"/>
 <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
 
 <stringbundleset id="stringbundleset">
   <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
   <stringbundle id="bundle_offlinePrompts" src="chrome://messenger/locale/offline.properties"/>
 </stringbundleset>
 
 <!-- Performance optimization...we include utilityOverlay.xul which defines some command sets
--- a/mail/base/content/msgMail3PaneWindow.js
+++ b/mail/base/content/msgMail3PaneWindow.js
@@ -389,16 +389,18 @@ function OnLoadMessenger()
     panelcontainer.addEventListener("InstallBrowserTheme",
                                     LightWeightThemeWebInstaller, false, true);
     panelcontainer.addEventListener("PreviewBrowserTheme",
                                     LightWeightThemeWebInstaller, false, true);
     panelcontainer.addEventListener("ResetBrowserThemePreview",
                                     LightWeightThemeWebInstaller, false, true);
   }
 
+  Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
+
   // This also registers the contentTabType ("contentTab")
   specialTabs.openSpecialTabsOnStartup();
 
   window.addEventListener("AppCommand", HandleAppCommandEvent, true);
 }
 
 function LoadPostAccountWizard()
 {
@@ -585,16 +587,18 @@ function OnUnloadMessenger()
   tabmail._teardown();
 
   var mailSession = Components.classes["@mozilla.org/messenger/services/session;1"]
                               .getService(Components.interfaces.nsIMsgMailSession);
   mailSession.RemoveFolderListener(folderListener);
 
   gPhishingDetector.shutdown();
 
+  Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed");
+
   // FIX ME - later we will be able to use onload from the overlay
   OnUnloadMsgHeaderPane();
 
   UnloadPanes();
 
   OnMailWindowUnload();
   try {
     mailInstrumentationManager.uninit();
new file mode 100644
--- /dev/null
+++ b/mail/base/content/plugins.js
@@ -0,0 +1,668 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* A note to the curious: a large portion of this code was copied over from
+ * mozilla/browser/base/content/browser.js
+ */
+
+#ifdef MOZ_CRASHREPORTER
+XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
+                                   "@mozilla.org/xre/app-info;1",
+                                   "nsICrashReporter");
+#endif
+
+function getPluginInfo(pluginElement)
+{
+  var tagMimetype;
+  var pluginsPage;
+  if (pluginElement instanceof HTMLAppletElement) {
+    tagMimetype = "application/x-java-vm";
+  } else {
+    if (pluginElement instanceof HTMLObjectElement) {
+      pluginsPage = pluginElement.getAttribute("codebase");
+    } else {
+      pluginsPage = pluginElement.getAttribute("pluginspage");
+    }
+
+    // only attempt if a pluginsPage is defined.
+    if (pluginsPage) {
+      var doc = pluginElement.ownerDocument;
+      var docShell = findChildShell(doc, gBrowser.docShell, null);
+      try {
+        pluginsPage = makeURI(pluginsPage, doc.characterSet, docShell.currentURI).spec;
+      } catch (ex) {
+        pluginsPage = "";
+      }
+    }
+
+    tagMimetype = pluginElement.QueryInterface(Components.interfaces.nsIObjectLoadingContent)
+                 .actualType;
+
+    if (tagMimetype == "") {
+      tagMimetype = pluginElement.type;
+    }
+  }
+
+  return {mimetype: tagMimetype, pluginsPage: pluginsPage};
+}
+
+/**
+ * Format a URL
+ * eg:
+ * echo formatURL("https://addons.mozilla.org/%LOCALE%/%APP%/%VERSION%/");
+ * > https://addons.mozilla.org/en-US/firefox/3.0a1/
+ *
+ * Currently supported built-ins are LOCALE, APP, and any value from nsIXULAppInfo, uppercased.
+ */
+function formatURL(aFormat, aIsPref) {
+  var formatter = Services.urlFormatter;
+  return aIsPref ? formatter.formatURLPref(aFormat) : formatter.formatURL(aFormat);
+}
+
+var gPluginHandler = {
+  addEventListeners: function ph_addEventListeners(browser) {
+    browser.addEventListener("PluginNotFound", gPluginHandler, true);
+    browser.addEventListener("PluginCrashed", gPluginHandler, true);
+    browser.addEventListener("PluginBlocklisted", gPluginHandler, true);
+    browser.addEventListener("PluginOutdated", gPluginHandler, true);
+    browser.addEventListener("PluginDisabled", gPluginHandler, true);
+    browser.addEventListener("NewPluginInstalled", gPluginHandler, true);
+  },
+
+  removeEventListeners: function ph_removeEventListeners(browser) {
+    browser.removeEventListener("PluginNotFound", gPluginHandler);
+    browser.removeEventListener("PluginCrashed", gPluginHandler);
+    browser.removeEventListener("PluginBlocklisted", gPluginHandler);
+    browser.removeEventListener("PluginOutdated", gPluginHandler);
+    browser.removeEventListener("PluginDisabled", gPluginHandler);
+    browser.removeEventListener("NewPluginInstalled", gPluginHandler);
+  },
+
+  get CrashSubmit() {
+    delete this.CrashSubmit;
+    Components.utils.import("resource://gre/modules/CrashSubmit.jsm", this);
+    return this.CrashSubmit;
+  },
+
+  // Map the plugin's name to a filtered version more suitable for user UI.
+  makeNicePluginName : function ph_makeNicePluginName(aName, aFilename) {
+    if (aName == "Shockwave Flash")
+      return "Adobe Flash";
+
+    // Clean up the plugin name by stripping off any trailing version numbers
+    // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar"
+    return aName.replace(/\bplug-?in\b/i, "").replace(/[\s\d\.\-\_\(\)]+$/, "");
+  },
+
+  isTooSmall : function ph_isTooSmall(plugin, overlay) {
+    // Is the <object>'s size too small to hold what we want to show?
+    let pluginRect = plugin.getBoundingClientRect();
+    // XXX bug 446693. The text-shadow on the submitted-report text at
+    //     the bottom causes scrollHeight to be larger than it should be.
+    let overflows = (overlay.scrollWidth > pluginRect.width) ||
+                    (overlay.scrollHeight - 5 > pluginRect.height);
+    return overflows;
+  },
+
+  addLinkClickCallback: function ph_addLinkClickCallback(linkNode, callbackName /*callbackArgs...*/) {
+    // XXX just doing (callback)(arg) was giving a same-origin error. bug?
+    let self = this;
+    let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
+    linkNode.addEventListener("click",
+                              function(evt) {
+                                if (!evt.isTrusted)
+                                  return;
+                                evt.preventDefault();
+                                if (callbackArgs.length == 0)
+                                  callbackArgs = [ evt ];
+                                (self[callbackName]).apply(self, callbackArgs);
+                              },
+                              true);
+
+    linkNode.addEventListener("keydown",
+                              function(evt) {
+                                if (!evt.isTrusted)
+                                  return;
+                                if (evt.keyCode == evt.DOM_VK_RETURN) {
+                                  evt.preventDefault();
+                                  if (callbackArgs.length == 0)
+                                    callbackArgs = [ evt ];
+                                  evt.preventDefault();
+                                  (self[callbackName]).apply(self, callbackArgs);
+                                }
+                              },
+                              true);
+  },
+
+  handleEvent : function ph_handleEvent(event) {
+    let self = gPluginHandler;
+    let plugin = event.target;
+    let doc = plugin.ownerDocument;
+
+    // We're expecting the target to be a plugin.
+    if (!(plugin instanceof Components.interfaces.nsIObjectLoadingContent))
+      return;
+
+    // Force a style flush, so that we ensure our binding is attached.
+    plugin.clientTop;
+
+    switch (event.type) {
+      case "PluginCrashed":
+        self.pluginInstanceCrashed(plugin, event);
+        break;
+
+      case "PluginNotFound":
+        // For non-object plugin tags, register a click handler to install the
+        // plugin. Object tags can, and often do, deal with that themselves,
+        // so don't stomp on the page developers toes.
+        if (!(plugin instanceof HTMLObjectElement)) {
+          // We don't yet check to see if there's actually an installer available.
+          let installStatus = doc.getAnonymousElementByAttribute(plugin, "class", "installStatus");
+          installStatus.setAttribute("status", "ready");
+          let iconStatus = doc.getAnonymousElementByAttribute(plugin, "class", "icon");
+          iconStatus.setAttribute("status", "ready");
+
+          let installLink = doc.getAnonymousElementByAttribute(plugin, "class", "installPluginLink");
+          self.addLinkClickCallback(installLink, "installSinglePlugin", plugin);
+        }
+        /* FALLTHRU */
+
+      case "PluginBlocklisted":
+      case "PluginOutdated":
+#ifdef XP_MACOSX
+      case "npapi-carbon-event-model-failure":
+#endif
+        self.pluginUnavailable(plugin, event.type);
+        break;
+
+      case "PluginDisabled":
+        let manageLink = doc.getAnonymousElementByAttribute(plugin, "class", "managePluginsLink");
+        self.addLinkClickCallback(manageLink, "managePlugins");
+        break;
+    }
+
+    // Hide the in-content UI if it's too big. The crashed plugin handler already did this.
+    if (event.type != "PluginCrashed") {
+      let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
+      if (self.isTooSmall(plugin, overlay))
+          overlay.style.visibility = "hidden";
+    }
+  },
+
+  newPluginInstalled : function ph_newPluginInstalled(event) {
+    // browser elements are anonymous so we can't just use target.
+    var browser = event.originalTarget;
+
+    // clear the plugin list, now that at least one plugin has been installed
+    browser.missingPlugins = null;
+
+    var notificationBox = getNotificationBox(browser.contentWindow);
+    var notification = notificationBox.getNotificationWithValue("missing-plugins");
+    if (notification)
+      notificationBox.removeNotification(notification);
+
+    // reload the browser to make the new plugin show.
+    browser.reload();
+  },
+
+  // Callback for user clicking on a missing (unsupported) plugin.
+  installSinglePlugin: function ph_installSinglePlugin(plugin) {
+    var missingPluginsArray = {};
+
+    var pluginInfo = getPluginInfo(plugin);
+    missingPluginsArray[pluginInfo.mimetype] = pluginInfo;
+
+    openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul",
+               "PFSWindow", "chrome,centerscreen,resizable=yes",
+               {plugins: missingPluginsArray, browser: getBrowser});
+  },
+
+  // Callback for user clicking on a disabled plugin
+  managePlugins: function ph_managePlugins(aEvent) {
+    openAddonsMgr("addons://list/plugin");
+  },
+
+  // Callback for user clicking "submit a report" link
+  submitReport : function ph_submitReport(pluginDumpID, browserDumpID) {
+    // The crash reporter wants a DOM element it can append an IFRAME to,
+    // which it uses to submit a form. Let's just give it curBrowser.
+
+    var curBrowser = document.getElementById('tabmail').getBrowserForSelectedTab();
+    this.CrashSubmit.submit(pluginDumpID, curBrowser, null, null);
+    if (browserDumpID)
+      this.CrashSubmit.submit(browserDumpID, curBrowser, null, null);
+  },
+
+  // Callback for user clicking a "reload page" link
+  reloadPage: function ph_reloadPage(browser) {
+    browser.reload();
+  },
+
+  // Callback for user clicking the help icon
+  openPluginCrashHelpPage: function ph_openHelpPage() {
+    // Grab the plugin crash support URL
+    let url = Services.urlFormatter.formatURLPref("plugins.crash.supportUrl");
+    // Now open up a content tab to display it in
+    let tabmail = document.getElementById('tabmail');
+    tabmail.openTab("contentTab", {contentPage: url,
+                                   background: false});
+  },
+
+  // event listener for missing/blocklisted/outdated/carbonFailure plugins.
+  pluginUnavailable: function ph_pluginUnavailable(plugin, eventType) {
+    let Cc = Components.classes;
+    let Ci = Components.interfaces;
+    var tabmail = document.getElementById('tabmail');
+    let browser = tabmail.getBrowserForDocument(plugin.ownerDocument
+                                                .defaultView).browser;
+
+    if (!browser.missingPlugins)
+      browser.missingPlugins = {};
+
+    var pluginInfo = getPluginInfo(plugin);
+    browser.missingPlugins[pluginInfo.mimetype] = pluginInfo;
+
+    var notificationBox = getNotificationBox(browser.contentWindow);
+
+    // Should only display one of these warnings per page.
+    // In order of priority, they are: outdated > missing > blocklisted
+    let outdatedNotification = notificationBox.getNotificationWithValue("outdated-plugins");
+    let blockedNotification  = notificationBox.getNotificationWithValue("blocked-plugins");
+    let missingNotification  = notificationBox.getNotificationWithValue("missing-plugins");
+
+    function showBlocklistInfo() {
+      var url = formatURL("extensions.blocklist.detailsURL", true);
+      tabmail.openTab("contentTab", {contentPage: url,
+                                     background: false});
+      return true;
+    }
+
+    function showOutdatedPluginsInfo() {
+      Services.prefs.setBoolPref("plugins.update.notifyUser", false);
+      var url = formatURL("plugins.update.url", true);
+      tabmail.openTab("contentTab", {contentPage: url,
+                                     background: false});
+      return true;
+    }
+
+    function showPluginsMissing() {
+      // get the urls of missing plugins
+      var curBrowser = tabmail.getBrowserForSelectedTab();
+      var missingPluginsArray = curBrowser.missingPlugins;
+      if (missingPluginsArray) {
+        openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul",
+                   "PFSWindow", "chrome,centerscreen,resizable=yes",
+                   {plugins: missingPluginsArray, browser: curBrowser});
+      }
+    }
+
+#ifdef XP_MACOSX
+    function carbonFailurePluginsRestartBrowser()
+    {
+      // Notify all windows that an application quit has been requested.
+      let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+                         createInstance(Ci.nsISupportsPRBool);
+      Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+      // Something aborted the quit process.
+      if (cancelQuit.data)
+        return;
+
+      let as = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+      as.quit(Ci.nsIAppStartup.eRestarti386 | Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+    }
+#endif
+
+    let messengerBundle = document.getElementById("bundle_messenger");
+
+    let notifications = {
+      PluginBlocklisted : {
+        barID: "blocked-plugins",
+        iconURL: "chrome://mozapps/skin/plugins/notifyPluginBlocked.png",
+        message: messengerBundle.getString("blockedpluginsMessage.title"),
+        buttons: [{
+          label: messengerBundle.getString("blockedpluginsMessage.infoButton.label"),
+          accessKey: messengerBundle.getString("blockedpluginsMessage.infoButton.accesskey"),
+          popup: null,
+          callback: showBlocklistInfo
+        },
+        {
+          label: messengerBundle.getString("blockedpluginsMessage.searchButton.label"),
+          accessKey: messengerBundle.getString("blockedpluginsMessage.searchButton.accesskey"),
+          popup: null,
+          callback: showOutdatedPluginsInfo
+        }],
+      },
+      PluginOutdated: {
+        barID: "outdated-plugins",
+        iconURL: "chrome://mozapps/skin/plugins/notifyPluginOutdated.png",
+        message: messengerBundle.getString("outdatedpluginsMessage.title"),
+        buttons: [{
+          label: messengerBundle.getString("outdatedpluginsMessage.updateButton.label"),
+          accessKey: messengerBundle.getString("outdatedpluginsMessage.updateButton.accesskey"),
+          popup: null,
+          callback: showOutdatedPluginsInfo
+        }],
+      },
+      PluginNotFound: {
+        barID: "missing-plugins",
+        iconURL: "chrome://mozapps/skin/plugins/notifyPluginGeneric.png",
+        message: messengerBundle.getString("missingpluginsMessage.title"),
+        buttons: [{
+          label: messengerBundle.getString("missingpluginsMessage.button.label"),
+          accessKey: messengerBundle.getString("missingpluginsMessage.button.accesskey"),
+          popup: null,
+          callback: showPluginsMissing
+        }],
+      },
+#ifdef XP_MACOSX
+      "npapi-carbon-event-model-failure": {
+        barID: "carbon-failure-plugins",
+        iconURL: "chrome://mozapps/skin/plugins/notifyPluginGeneric.png",
+        message: messengerBundle.getString("carbonFailurePluginsMessage.message"),
+        buttons: [{
+          label: messengerBundle.getString("carbonFailurePluginsMessage.restartButton.label"),
+          accessKey: messengerBundle.getString("carbonFailurePluginsMessage.restartButton.accesskey"),
+          popup: null,
+          callback: carbonFailurePluginsRestartBrowser
+        }],
+      }
+#endif
+    };
+
+
+    // If there is already an outdated plugin notification then do nothing
+    if (outdatedNotification)
+      return;
+
+#ifdef XP_MACOSX
+    if (eventType == "npapi-carbon-event-model-failure") {
+      if (Services.prefs.getBoolPref("plugins.hide_infobar_for_carbon_failure_plugin"))
+        return;
+
+      let carbonFailureNotification =
+        notificationBox.getNotificationWithValue("carbon-failure-plugins");
+
+      if (carbonFailureNotification)
+         carbonFailureNotification.close();
+
+      let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils);
+      // if this is not a Universal build, just follow PluginNotFound path
+      if (!macutils.isUniversalBinary)
+        eventType = "PluginNotFound";
+    }
+#endif
+
+    if (eventType == "PluginBlocklisted") {
+      if (Services.prefs.getBoolPref("plugins.hide_infobar_for_missing_plugin"))
+        return;
+
+      if (blockedNotification || missingNotification)
+        return;
+    }
+    else if (eventType == "PluginOutdated") {
+      if (Services.prefs.getBoolPref("plugins.hide_infobar_for_outdated_plugin"))
+        return;
+
+      // Cancel any notification about blocklisting/missing plugins
+      if (blockedNotification)
+        blockedNotification.close();
+      if (missingNotification)
+        missingNotification.close();
+    }
+    else if (eventType == "PluginNotFound") {
+      if (Services.prefs.getBoolPref("plugins.hide_infobar_for_missing_plugin"))
+        return;
+
+
+      if (missingNotification)
+        return;
+
+      // Cancel any notification about blocklisting plugins
+      if (blockedNotification)
+        blockedNotification.close();
+    }
+
+    let notify = notifications[eventType];
+    notificationBox.appendNotification(notify.message, notify.barID, notify.iconURL,
+                                       notificationBox.PRIORITY_WARNING_MEDIUM,
+                                       notify.buttons);
+  },
+
+  // Crashed-plugin observer. Notified once per plugin crash, before events
+  // are dispatched to individual plugin instances.
+  pluginCrashed : function(subject, topic, data) {
+    let propertyBag = subject;
+    if (!(propertyBag instanceof Components.interfaces.nsIPropertyBag2) ||
+        !(propertyBag instanceof Components.interfaces.nsIWritablePropertyBag2))
+     return;
+
+#ifdef MOZ_CRASHREPORTER
+    let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
+    let browserDumpID = propertyBag.getPropertyAsAString("browserDumpID");
+    let shouldSubmit = gCrashReporter.submitReports;
+    let doPrompt = true; // XXX followup to get via gCrashReporter
+
+    // Submit automatically when appropriate.
+    if (pluginDumpID && shouldSubmit && !doPrompt) {
+      this.submitReport(pluginDumpID, browserDumpID);
+      // Submission is async, so we can't easily show failure UI.
+      propertyBag.setPropertyAsBool("submittedCrashReport", true);
+    }
+#endif
+  },
+
+  // Crashed-plugin event listener. Called for every instance of a
+  // plugin in content.
+  pluginInstanceCrashed: function (plugin, aEvent) {
+    // Ensure the plugin and event are of the right type.
+    if (!(aEvent instanceof Components.interfaces.nsIDOMDataContainerEvent))
+      return;
+
+    let submittedReport = aEvent.getData("submittedCrashReport");
+    let doPrompt = true; // XXX followup for .getData("doPrompt");
+    let submitReports = true; // XXX followup for .getData("submitReports");
+    let pluginName = aEvent.getData("pluginName");
+    let pluginFilename = aEvent.getData("pluginFilename");
+    let pluginDumpID = aEvent.getData("pluginDumpID");
+    let browserDumpID = aEvent.getData("browserDumpID");
+    let messengerBundle = document.getElementById("bundle_messenger");
+    let tabmail = document.getElementById('tabmail');
+
+    // Remap the plugin name to a more user-presentable form.
+    pluginName = this.makeNicePluginName(pluginName, pluginFilename);
+
+    let messageString = messengerBundle.getFormattedString("crashedpluginsMessage.title", [pluginName]);
+
+    //
+    // Configure the crashed-plugin placeholder.
+    //
+    let doc = plugin.ownerDocument;
+    let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
+    let statusDiv = doc.getAnonymousElementByAttribute(plugin, "class", "submitStatus");
+#ifdef MOZ_CRASHREPORTER
+    let status;
+
+    // Determine which message to show regarding crash reports.
+    if (submittedReport) { // submitReports && !doPrompt, handled in observer
+      status = "submitted";
+    }
+    else if (!submitReports && !doPrompt) {
+      status = "noSubmit";
+    }
+    else { // doPrompt
+      status = "please";
+      // XXX can we make the link target actually be blank?
+      let pleaseLink = doc.getAnonymousElementByAttribute(
+                            plugin, "class", "pleaseSubmitLink");
+      this.addLinkClickCallback(pleaseLink, "submitReport",
+                                pluginDumpID, browserDumpID);
+    }
+
+    // If we don't have a minidumpID, we can't (or didn't) submit anything.
+    // This can happen if the plugin is killed from the task manager.
+    if (!pluginDumpID) {
+      status = "noReport";
+    }
+
+    statusDiv.setAttribute("status", status);
+
+    let bottomLinks = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgBottomLinks");
+    bottomLinks.style.display = "block";
+    let helpIcon = doc.getAnonymousElementByAttribute(plugin, "class", "helpIcon");
+    this.addLinkClickCallback(helpIcon, "openPluginCrashHelpPage");
+
+    // If we're showing the link to manually trigger report submission, we'll
+    // want to be able to update all the instances of the UI for this crash to
+    // show an updated message when a report is submitted.
+    if (doPrompt) {
+      let observer = {
+        QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver,
+                                               Components.interfaces.nsISupportsWeakReference]),
+        observe : function(subject, topic, data) {
+          let propertyBag = subject;
+          if (!(propertyBag instanceof Components.interfaces.nsIPropertyBag2))
+            return;
+          // Ignore notifications for other crashes.
+          if (propertyBag.get("minidumpID") != pluginDumpID)
+            return;
+          statusDiv.setAttribute("status", data);
+        },
+
+        handleEvent : function(event) {
+            // Not expected to be called, just here for the closure.
+        }
+      };
+
+      // Use a weak reference, so we don't have to remove it...
+      Services.obs.addObserver(observer, "crash-report-status", true);
+      // ...alas, now we need something to hold a strong reference to prevent
+      // it from being GC. But I don't want to manually manage the reference's
+      // lifetime (which should be no greater than the page).
+      // Clever solution? Use a closue with an event listener on the document.
+      // When the doc goes away, so do the listener references and the closure.
+      doc.addEventListener("mozCleverClosureHack", observer, false);
+    }
+#endif
+
+    let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgCrashed");
+    crashText.textContent = messageString;
+    let browser = tabmail.getBrowserForSelectedTab();
+
+    let link = doc.getAnonymousElementByAttribute(plugin, "class", "reloadLink");
+    this.addLinkClickCallback(link, "reloadPage", browser);
+
+    let notificationBox = getNotificationBox(browser.contentWindow);
+
+    // Is the <object>'s size too small to hold what we want to show?
+    if (this.isTooSmall(plugin, overlay)) {
+        // Hide the overlay's contents. Use visibility style, so that it
+        // doesn't collapse down to 0x0.
+        overlay.style.visibility = "hidden";
+        // If another plugin on the page was large enough to show our UI, we
+        // don't want to show a notification bar.
+        if (!doc.mozNoPluginCrashedNotification)
+          showNotificationBar(pluginDumpID, browserDumpID);
+    } else {
+        // If a previous plugin on the page was too small and resulted in
+        // adding a notification bar, then remove it because this plugin
+        // instance it big enough to serve as in-content notification.
+        hideNotificationBar();
+        doc.mozNoPluginCrashedNotification = true;
+    }
+
+    function hideNotificationBar() {
+      let notification = notificationBox.getNotificationWithValue("plugin-crashed");
+      if (notification)
+        notificationBox.removeNotification(notification, true);
+    }
+
+    function showNotificationBar(pluginDumpID, browserDumpID) {
+      // If there's already an existing notification bar, don't do anything.
+      let messengerBundle = document.getElementById("bundle_messenger");
+      let notification = notificationBox.getNotificationWithValue("plugin-crashed");
+      if (notification)
+        return;
+
+      // Configure the notification bar
+      let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+      let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png";
+      let reloadLabel = messengerBundle.getString("crashedpluginsMessage.reloadButton.label");
+      let reloadKey   = messengerBundle.getString("crashedpluginsMessage.reloadButton.accesskey");
+      let submitLabel = messengerBundle.getString("crashedpluginsMessage.submitButton.label");
+      let submitKey   = messengerBundle.getString("crashedpluginsMessage.submitButton.accesskey");
+
+      let buttons = [{
+        label: reloadLabel,
+        accessKey: reloadKey,
+        popup: null,
+        callback: function() { browser.reload(); },
+      }];
+#ifdef MOZ_CRASHREPORTER
+      let submitButton = {
+        label: submitLabel,
+        accessKey: submitKey,
+        popup: null,
+          callback: function() { gPluginHandler.submitReport(pluginDumpID, browserDumpID); },
+      };
+      if (pluginDumpID)
+        buttons.push(submitButton);
+#endif
+
+      let notification = notificationBox.appendNotification(messageString, "plugin-crashed",
+                                                            iconURL, priority, buttons);
+
+      // Add the "learn more" link.
+      let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+      let link = notification.ownerDocument.createElementNS(XULNS, "label");
+      let crashHelpUrl = Services.urlFormatter
+                                 .formatURLPref("plugins.crash.supportUrl");
+      link.className = "text-link";
+      link.setAttribute("value", messengerBundle.getString("crashedpluginsMessage.learnMore"));
+      link.href = crashHelpUrl;
+      let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
+      description.appendChild(link);
+
+      // Remove the notfication when the page is reloaded.
+      doc.defaultView.top.addEventListener("unload", function() {
+        notificationBox.removeNotification(notification);
+      }, false);
+    }
+
+  }
+};
+
--- a/mail/base/content/specialTabs.js
+++ b/mail/base/content/specialTabs.js
@@ -403,16 +403,17 @@ var specialTabs = {
                           "specialTabs.defaultClickHandler(event);";
       aTab.browser.setAttribute("onclick", aTab.clickHandler);
 
       // Set this attribute so that when favicons fail to load, we remove the
       // image attribute and just show the default tab icon.
       aTab.tabNode.setAttribute("onerror", "this.removeAttribute('image');");
 
       aTab.browser.addEventListener("DOMLinkAdded", DOMLinkHandler, false);
+      gPluginHandler.addEventListeners(aTab.browser);
 
       // Now initialise the find bar.
       aTab.findbar = aTab.panel.getElementsByTagName("findbar")[0];
       aTab.findbar.setAttribute("browserid",
                                 "contentTabBrowser" + this.lastBrowserId);
 
       // Default to reload being disabled.
       aTab.reloadEnabled = false;
@@ -457,16 +458,17 @@ var specialTabs = {
         && !docShell.contentViewer.permitUnload());
     },
     closeTab: function onTabClosed(aTab) {
       aTab.browser.removeEventListener("DOMTitleChanged",
                                        aTab.titleListener, true);
       aTab.browser.removeEventListener("DOMWindowClose",
                                        aTab.closeListener, true);
       aTab.browser.removeEventListener("DOMLinkAdded", DOMLinkHandler, false);
+      gPluginHandler.removeEventListeners(aTab.browser);
       aTab.browser.webProgress.removeProgressListener(aTab.filter);
       aTab.filter.removeProgressListener(aTab.progressListener);
       aTab.browser.destroy();
     },
     saveTabState: function onSaveTabState(aTab) {
       aTab.browser.setAttribute("type", "content-targetable");
     },
     showTab: function onShowTab(aTab) {
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -41,16 +41,17 @@ messenger.jar:
     content/messenger/commandglue.js                (content/commandglue.js)
     content/messenger/widgetglue.js                 (content/widgetglue.js)
 *   content/messenger/SearchDialog.xul              (content/SearchDialog.xul)
     content/messenger/SearchDialog.js               (content/SearchDialog.js)
 *   content/messenger/ABSearchDialog.xul            (content/ABSearchDialog.xul)
 *   content/messenger/ABSearchDialog.js             (content/ABSearchDialog.js)
 *   content/messenger/FilterListDialog.xul          (content/FilterListDialog.xul)
 *   content/messenger/FilterListDialog.js           (content/FilterListDialog.js)
+*   content/messenger/plugins.js                    (content/plugins.js)
     content/messenger/specialTabs.js                (content/specialTabs.js)
     content/messenger/specialTabs.xul               (content/specialTabs.xul)
 *   content/messenger/subscribe.xul                 (content/subscribe.xul)
     content/messenger/subscribe.js                  (content/subscribe.js)
 *   content/messenger/aboutDialog.xul               (content/aboutDialog.xul)
 *   content/messenger/aboutDialog.js                (content/aboutDialog.js)
 *   content/messenger/aboutRights.xhtml             (content/aboutRights.xhtml)
     content/messenger/featureConfigurator.xhtml     (content/featureConfigurator.xhtml)
--- a/mail/locales/en-US/chrome/messenger/messenger.properties
+++ b/mail/locales/en-US/chrome/messenger/messenger.properties
@@ -694,8 +694,30 @@ update.resumeButton.accesskey=D
 update.openUpdateUI.applyButton.label=Apply Update…
 update.openUpdateUI.applyButton.accesskey=A
 update.restart.applyButton.label=Apply Update
 update.restart.applyButton.accesskey=A
 update.openUpdateUI.upgradeButton.label=Upgrade Now…
 update.openUpdateUI.upgradeButton.accesskey=U
 update.restart.upgradeButton.label=Upgrade Now
 update.restart.upgradeButton.accesskey=U
+
+# missing plugin installer
+missingpluginsMessage.title=Additional plugins are required to display all the media on this page.
+missingpluginsMessage.button.label=Install Missing Plugins…
+missingpluginsMessage.button.accesskey=I
+outdatedpluginsMessage.title=Some plugins used by this page are out of date.
+outdatedpluginsMessage.updateButton.label=Update Plugins…
+outdatedpluginsMessage.updateButton.accesskey=U
+blockedpluginsMessage.title=Some plugins required by this page have been blocked for your protection.
+blockedpluginsMessage.infoButton.label=Details…
+blockedpluginsMessage.infoButton.accesskey=D
+blockedpluginsMessage.searchButton.label=Update Plugins…
+blockedpluginsMessage.searchButton.accesskey=U
+crashedpluginsMessage.title=The %S plugin has crashed.
+crashedpluginsMessage.reloadButton.label=Reload page
+crashedpluginsMessage.reloadButton.accesskey=R
+crashedpluginsMessage.submitButton.label=Submit a crash report
+crashedpluginsMessage.submitButton.accesskey=S
+crashedpluginsMessage.learnMore=Learn More…
+carbonFailurePluginsMessage.message=This page asks to use a plugin that can only run in 32-bit mode
+carbonFailurePluginsMessage.restartButton.label=Restart in 32-bit mode
+carbonFailurePluginsMessage.restartButton.accesskey=R
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/html/blocklist.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+  <pluginItems>
+    <pluginItem>
+      <match name="name" exp="Test Plug-in"/>
+      <versionRange severity="0"/>
+    </pluginItem>
+  </pluginItems>
+</blocklist>
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/html/blocklist_details.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>Plugin Blocklist Details</title>
+  </head>
+  <body bgcolor="#FFFFFF">
+    <h1>Plugin Blocklist Details Page</h1>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/html/plugin.html
@@ -0,0 +1,9 @@
+<html>
+  <head>
+    <title>Plugin Test</title>
+  </head>
+  <body bgcolor="#FFFFFF">
+    <h1>Plugin Test</h1>
+    <embed id="test-plugin" type="application/x-test" width="200" height="200"></embed>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/html/plugin_crashed_help.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>Plugin Crashed Help</title>
+  </head>
+  <body bgcolor="#FFFFFF">
+    <h1>Plugin Crashed Help</h1>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/html/plugin_update.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>Plugin Update Page</title>
+  </head>
+  <body bgcolor="#FFFFFF">
+    <h1>Plugin Update Page</h1>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/html/unknown-plugin.html
@@ -0,0 +1,9 @@
+<html>
+  <head>
+    <title>Unknown Plugin Test</title>
+  </head>
+  <body bgcolor="#FFFFFF">
+    <h1>Unknown Plugin Test</h1>
+    <embed id="test-plugin" type="application/x-test-unknown" width="200" height="200"></embed>
+  </body>
+</html>
--- a/mail/test/mozmill/content-tabs/test-lwthemes.js
+++ b/mail/test/mozmill/content-tabs/test-lwthemes.js
@@ -60,44 +60,16 @@ var setupModule = function (module) {
   let fdh = collector.getModule('folder-display-helpers');
   fdh.installInto(module);
   let cth = collector.getModule('content-tab-helpers');
   cth.installInto(module);
 };
 
 const ALERT_TIMEOUT = 10000;
 
-let AlertWatcher = {
-  planForAlert: function(aController) {
-    this.alerted = false;
-    aController.window.document.addEventListener("AlertActive",
-                                                 this.alertActive, false);
-  },
-  waitForAlert: function(aController) {
-    if (!this.alerted) {
-      aController.waitFor(function () this.alerted, "Timeout waiting for alert",
-                          ALERT_TIMEOUT, 100, this);
-    }
-    // Double check the notification box has finished animating.
-    let notificationBox =
-      mc.tabmail.selectedTab.panel.getElementsByTagName("notificationbox")[0];
-    if (notificationBox && notificationBox._animating)
-      aController.waitFor(function () !notificationBox._animating,
-                          "Timeout waiting for notification box animation to finish",
-                          ALERT_TIMEOUT, 100);
-
-    aController.window.document.removeEventListener("AlertActive",
-                                                    this.alertActive, false);
-  },
-  alerted: false,
-  alertActive: function() {
-    AlertWatcher.alerted = true;
-  }
-};
-
 function check_and_click_notification_box_action_in_current_tab(totalButtons,
                                                                 selectButton) {
   let notificationBox =
     mc.tabmail.selectedTab.panel.getElementsByTagName("notificationbox")[0];
 
   // This is a crude check to see that we've got the number of buttons we expect
   // and hence this is the right notification that is being shown.
   let buttons = notificationBox.currentNotification.getElementsByTagName("button");
@@ -118,28 +90,28 @@ function currentLwTheme() {
 }
 
 function install_theme(themeNo, previousThemeNo) {
   let notificationBox =
     mc.tabmail.selectedTab.panel.getElementsByTagName("notificationbox")[0];
 
   // Clicking the button will bring up a notification box requesting to allow
   // installation of the theme
-  AlertWatcher.planForAlert(mc);
+  NotificationWatcher.planForNotification(mc);
   mc.click(new elib.Elem(mc.window.content.document
                            .getElementById("install" + themeNo)));
-  AlertWatcher.waitForAlert(mc);
+  NotificationWatcher.waitForNotification(mc);
 
   // We're going to acknowledge the theme installation being allowed, and
   // in doing so, the theme will be installed. However, we also will get a new
   // notification box displayed saying the installation is complete, so we'll
   // have to handle that here as well.
-  AlertWatcher.planForAlert(mc);
+  NotificationWatcher.planForNotification(mc);
   check_and_click_notification_box_action_in_current_tab(1, 0);
-  AlertWatcher.waitForAlert(mc);
+  NotificationWatcher.waitForNotification(mc);
 
   // Before we do anything more, check what we've got installed.
   if (!currentLwTheme())
     throw new Error("No lightweight theme selected when there should have been.");
 
   if (currentLwTheme().id != ("test-0" + themeNo))
     throw new Error("Incorrect theme installed, expected: test-0" + themeNo +
                     " got " + currentLwTheme().id);
@@ -155,28 +127,28 @@ function install_theme(themeNo, previous
       throw new Error("No lightweight theme installed after selecting undo");
 
     if (currentLwTheme().id != ("test-0" + previousThemeNo))
       throw new Error("After undo expected: test-0" + previousThemeNo +
                       " but got " + currentLwTheme().id);
   }
 
   // Now Click again to install, and this time, we'll leave it there.
-  AlertWatcher.planForAlert(mc);
+  NotificationWatcher.planForNotification(mc);
   mc.click(new elib.Elem(mc.window.content.document
                            .getElementById("install" + themeNo)));
-  AlertWatcher.waitForAlert(mc);
+  NotificationWatcher.waitForNotification(mc);
 
   // We're going to acknowledge the theme installation being allowed, and
   // in doing so, the theme will be installed. However, we also will get a new
   // notification box displayed saying the installation is complete, so we'll
   // have to handle that here as well.
-  AlertWatcher.planForAlert(mc);
+  NotificationWatcher.planForNotification(mc);
   check_and_click_notification_box_action_in_current_tab(1, 0);
-  AlertWatcher.waitForAlert(mc);
+  NotificationWatcher.waitForNotification(mc);
 
   // Now just close the notification box
   close_notification_box_in_current_tab();
 
   // And one final check for what we've got installed.
   if (!currentLwTheme())
     throw new Error("No lightweight theme selected when there should have been.");
 
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/test-plugin-blocked.js
@@ -0,0 +1,137 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var MODULE_NAME = 'test-plugin-blocked';
+
+var RELATIVE_ROOT = '../shared-modules';
+var MODULE_REQUIRES = ['folder-display-helpers', 'content-tab-helpers'];
+
+var controller = {};
+Components.utils.import('resource://mozmill/modules/controller.js', controller);
+var elib = {};
+Components.utils.import('resource://mozmill/modules/elementslib.js', elib);
+
+Components.utils.import('resource://gre/modules/Services.jsm');
+
+var gOldStartUrl = null;
+var gOldBlDetailsUrl = null;
+var gOldPluginUpdateUrl = null;
+
+const kPluginId = "test-plugin";
+const kStartPagePref = "mailnews.start_page.override_url";
+const kBlDetailsPagePref = "extensions.blocklist.detailsURL";
+const kPluginsUpdatePref = "plugins.update.url";
+// RELATIVE_ROOT messes with the collector, so we have to bring the path back
+// so we get the right path for the resources.
+const kUrl = collector.addHttpResource('../content-tabs/html', '');
+const kPluginUrl = kUrl + "plugin.html";
+const kBlDetailsUrl = kUrl + "blocklist_details.html";
+const kPluginUpdateUrl = kUrl + "plugin_update.html";
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let cth = collector.getModule('content-tab-helpers');
+  cth.installInto(module);
+
+  // Set the pref so that what's new opens a local url - we'll save the old
+  // url and put it back in the module teardown.
+  gOldStartUrl = Services.prefs.getCharPref(kStartPagePref);
+  gOldBlDetailsUrl = Services.prefs.getCharPref(kBlDetailsPagePref);
+  gOldPluginUpdateUrl = Services.prefs.getCharPref(kPluginsUpdatePref);
+
+  Services.prefs.setCharPref(kStartPagePref, kPluginUrl);
+  Services.prefs.setCharPref(kBlDetailsPagePref, kBlDetailsUrl);
+  Services.prefs.setCharPref(kPluginsUpdatePref, kPluginUpdateUrl);
+};
+
+function teardownModule(module) {
+  Services.prefs.setCharPref(kStartPagePref, gOldStartUrl);
+  Services.prefs.setCharPref(kBlDetailsPagePref, gOldBlDetailsUrl);
+  Services.prefs.setCharPref(kPluginsUpdatePref, gOldPluginUpdateUrl);
+}
+
+function setupTest() {
+  let plugin = get_test_plugin();
+  plugin.disabled = false;
+  plugin.blocklisted = true;
+}
+
+function teardownTest() {
+  let plugin = get_test_plugin();
+  plugin.disabled = false;
+  plugin.blocklisted = false;
+}
+
+/* Tests that the notification bar appears for plugins that
+ * are blocklisted.  Ensures that the notification bar gives
+ * links to the human-readable blocklist, as well as the
+ * plugin update page.
+ */
+function test_blocklisted_plugin_notification() {
+  // Prepare to capture the notification bar
+  NotificationWatcher.planForNotification(mc);
+  let pluginTab = open_content_tab_with_click(mc.menus.helpMenu.whatsNew,
+                                              kPluginUrl);
+  NotificationWatcher.waitForNotification(mc);
+
+  // If we got here, then the notification bar appeared.  Now
+  // let's make sure it displayed the right message.
+  let notificationBar = get_notification_bar_for_tab(mc.tabmail.selectedTab);
+  assert_not_equals(null, notificationBar, "Could not get notification bar");
+  let blNotification = notificationBar.getNotificationWithValue("blocked-plugins");
+  assert_not_equals(null, blNotification, "Notification value was not correct");
+
+  // buttons[0] should be the "more info" button, and buttons[1]
+  // should be the "update my plugins" button.
+  let buttons = notificationBar.getElementsByTagName("button");
+
+  // Let's make sure that the "more info" button opens up a tab
+  // and takes us to the right place.
+  let detailsTab = open_content_tab_with_click(buttons[0], kBlDetailsUrl);
+  assert_tab_has_title(detailsTab, "Plugin Blocklist Details");
+  mc.tabmail.closeTab(detailsTab);
+
+  // Let's make sure that the "update my plugins" button opens up
+  // a tab and takes us to the right place.
+  let updateTab = open_content_tab_with_click(buttons[1], kPluginUpdateUrl);
+  assert_tab_has_title(updateTab, "Plugin Update Page");
+  mc.tabmail.closeTab(updateTab);
+
+  // Close the tab to finish up.
+  mc.tabmail.closeTab(pluginTab);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/test-plugin-crashing.js
@@ -0,0 +1,259 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var MODULE_NAME = 'test-plugin-crashing';
+
+var RELATIVE_ROOT = '../shared-modules';
+var MODULE_REQUIRES = ['folder-display-helpers', 'content-tab-helpers'];
+
+var frame = {};
+Components.utils.import('resource://mozmill/modules/frame.js', frame);
+var controller = {};
+Components.utils.import('resource://mozmill/modules/controller.js', controller);
+var elib = {};
+Components.utils.import('resource://mozmill/modules/elementslib.js', elib);
+
+Components.utils.import('resource://gre/modules/Services.jsm');
+
+var gContentWindow = null;
+var gJSObject = null;
+var gTabDoc = null;
+var gOldStartPage = null;
+
+const kPluginId = "test-plugin";
+const kStartPagePref = "mailnews.start_page.override_url";
+const kPluginCrashDocPref = "plugins.crash.supportUrl";
+// RELATIVE_ROOT messes with the collector, so we have to bring the path back
+// so we get the right path for the resources.
+const kUrl = collector.addHttpResource('../content-tabs/html', '');
+const kPluginUrl = kUrl + "plugin.html";
+const kPluginCrashDocUrl = kUrl + "plugin_crashed_help.html";
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let cth = collector.getModule('content-tab-helpers');
+  cth.installInto(module);
+
+  // Set the pref so that what's new opens a local url - we'll save the old
+  // url and put it back in the module teardown.
+  gOldStartPage = Services.prefs.getCharPref(kStartPagePref);
+  gOldPluginCrashDocPage = Services.prefs.getCharPref(kPluginCrashDocPref);
+
+  Services.prefs.setCharPref(kStartPagePref, kPluginUrl);
+  Services.prefs.setCharPref(kPluginCrashDocPref, kPluginCrashDocUrl);
+
+  if (!plugins_run_in_separate_processes(mc)) {
+    let funcsToSkip = [test_can_crash_plugin,
+                       test_crashed_plugin_notification_bar,
+                       test_crashed_plugin_notification_inline];
+
+    funcsToSkip.forEach(function(func) {
+      func.__force_skip__ = true;
+    });
+  }
+};
+
+function teardownModule(module) {
+  Services.prefs.setCharPref(kStartPagePref, gOldStartPage);
+  Services.prefs.setCharPref(kPluginCrashDocPref, gOldPluginCrashDocPage);
+}
+
+function setupTest() {
+  let tab = open_content_tab_with_click(mc.menus.helpMenu.whatsNew, kPluginUrl);
+  assert_tab_has_title(tab, "Plugin Test");
+
+  // Check that window.content is set up correctly wrt content-primary and
+  // content-targetable.
+  if (mc.window.content.location != kPluginUrl)
+    throw new Error("window.content is not set to the url loaded, incorrect type=\"...\"?");
+
+  gContentWindow = mc.tabmail.selectedTab.browser.contentWindow;
+  gJSObject = gContentWindow.wrappedJSObject;
+
+  // Strangely, in order to manipulate the embedded plugin,
+  // we have to use getElementById within the context of the
+  // wrappedJSObject of the content tab browser.
+  gTabDoc = gJSObject.window.document;
+
+}
+
+function teardownTest() {
+  let tab = mc.tabmail.selectedTab;
+  mc.tabmail.closeTab(tab);
+}
+
+/* PluginCrashObserver lets us plan for and wait for plugin crashes. After
+ * a plugin has crashed, PluginCrashObserver cleans up the minidump files
+ * left behind.
+ *
+ * IMPORTANT:  Calls to planForCrash must be followed by waitForCrash in
+ * order to remove PluginCrashObserver from the nsIObserverService.
+ */
+let PluginCrashObserver = {
+  _sawCrash: false,
+
+  planForCrash: function(aController) {
+    this._sawCrash = false;
+    Services.obs.addObserver(this, "plugin-crashed", false);
+  },
+
+  waitForCrash: function(aController) {
+    if (!this._sawCrash)
+      aController.waitFor(function() this._sawCrash, "Timeout waiting for crash",
+                          5000, 100, this);
+
+    Services.obs.removeObserver(this, "plugin-crashed");
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic != "plugin-crashed")
+      return;
+
+    try {
+      this.removeMinidump(
+        aSubject.QueryInterface(Components.interfaces.nsIPropertyBag2));
+    } catch (ex) {
+      Cu.reportError(ex);
+      frame.events.fail({exception: ex, test: frame.events.currentTest});
+    }
+  },
+
+  removeMinidump: function PluginCrashObserver_removeMinidump(aPropBag) {
+    this._sawCrash = true;
+
+    let profD = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile);
+    profD.append("minidumps");
+
+    // Let's check to see if a minidump was created.  If so, delete
+    // it (along with the .extra file)
+    let crashId = aPropBag.getPropertyAsAString("pluginDumpID");
+    let dumpFile = profD.clone();
+    dumpFile.append(crashId + ".dmp");
+    let extraFile = profD.clone();
+    extraFile.append(crashId + ".extra");
+
+    if (dumpFile.exists())
+      dumpFile.remove(false);
+
+    if (extraFile.exists())
+      extraFile.remove(false);
+  }
+}
+
+/* Crash the plugin */
+function crash_plugin() {
+  try {
+    let plugin = gTabDoc.getElementById(kPluginId);
+    PluginCrashObserver.planForCrash(mc);
+    plugin.crash();
+  } catch(e) {
+    PluginCrashObserver.waitForCrash(mc);
+    return true;
+  }
+  return false;
+}
+
+/* A quick sanity check - let's ensure that we can actually
+ * crash the plugin.
+ */
+function test_can_crash_plugin() {
+  assert_true(crash_plugin());
+}
+
+/* Test to check that if a plugin crashes, and the plugin's
+ * <object> is too small to display a message, then a
+ * notification box appears to tell us about the crash.
+ */
+function test_crashed_plugin_notification_bar() {
+  let plugin = gTabDoc.getElementById(kPluginId);
+  plugin.style.width = '10px';
+  plugin.style.height = '10px';
+
+  NotificationWatcher.planForNotification(mc);
+  assert_true(crash_plugin());
+  NotificationWatcher.waitForNotification(mc);
+}
+
+/* Test that if a plugin crashes, and the plugin's <object>
+ * is large enough to display a message, it'll display the
+ * appropriate crash message.
+ */
+function test_crashed_plugin_notification_inline() {
+  let plugin = gTabDoc.getElementById(kPluginId);
+  plugin.style.width = '200px';
+  plugin.style.height = '200px';
+
+  assert_true(crash_plugin());
+
+  /* This function attempts to return the status div on the
+   * crashed plugin widget.  Returns null on failure.
+   */
+  function getStatusDiv() {
+    let submitDiv = gContentWindow.document
+                                  .getAnonymousElementByAttribute(plugin,
+                                                                  "class",
+                                                                  "submitStatus");
+
+    if (!submitDiv)
+      return null;
+
+    return submitDiv;
+  }
+
+  mc.waitFor(function() (getStatusDiv() != null),
+             "Timed out waiting for plugin status div to appear");
+
+  let submitDiv = getStatusDiv();
+
+  // Depending on the environment we're running this test on,
+  // the status attribute might be "noReport" or "please".
+  let statusString = submitDiv.getAttribute("status");
+  assert_true(statusString == "noReport" || statusString == "please",
+              "Expected the status to be \"noReport\" or \"please\"");
+
+  // Make sure that the help link in the inline notification works.
+  let helpIcon = gContentWindow.document
+                               .getAnonymousElementByAttribute(plugin,
+                                                               "class",
+                                                               "helpIcon");
+  assert_not_equals(null, helpIcon, "Help Icon should have been available");
+
+  let helpTab = open_content_tab_with_click(helpIcon, kPluginCrashDocUrl);
+  assert_tab_has_title(helpTab, "Plugin Crashed Help");
+  mc.tabmail.closeTab(helpTab);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/test-plugin-outdated.js
@@ -0,0 +1,149 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var MODULE_NAME = 'test-plugin-outdated';
+
+var RELATIVE_ROOT = '../shared-modules';
+var MODULE_REQUIRES = ['folder-display-helpers', 'content-tab-helpers'];
+
+var controller = {};
+Components.utils.import('resource://mozmill/modules/controller.js', controller);
+var elib = {};
+Components.utils.import('resource://mozmill/modules/elementslib.js', elib);
+
+Components.utils.import('resource://gre/modules/Services.jsm');
+
+var gOldStartUrl = null;
+var gOldPluginUpdateUrl = null;
+var gHadBlocklist = false;
+
+const kPluginId = "test-plugin";
+const kStartPagePref = "mailnews.start_page.override_url";
+const kPluginsUpdatePref = "plugins.update.url";
+const kBlEnabledPref = "extensions.blocklist.enabled";
+// RELATIVE_ROOT messes with the collector, so we have to bring the path back
+// so we get the right path for the resources.
+const kUrl = collector.addHttpResource('../content-tabs/html', '');
+const kPluginUrl = kUrl + "plugin.html";
+const kPluginUpdateUrl = kUrl + "plugin_update.html";
+const kBlocklist = "blocklist.xml";
+const kBlocklistOld = "blocklist-old.xml";
+const kNewBlocklistPath = "./html/" + kBlocklist;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let cth = collector.getModule('content-tab-helpers');
+  cth.installInto(module);
+
+  // Set the pref so that what's new opens a local url - we'll save the old
+  // url and put it back in the module teardown.
+  gOldStartUrl = Services.prefs.getCharPref(kStartPagePref);
+
+  // Stash the old plugin update URL so we can put it back on module
+  // teardown
+  gOldPluginUpdateUrl = Services.prefs.getCharPref(kPluginsUpdatePref);
+
+  Services.prefs.setCharPref(kStartPagePref, kPluginUrl);
+  Services.prefs.setCharPref(kPluginsUpdatePref, kPluginUpdateUrl);
+
+  // See if there's a local blocklist.xml in the profile directory.
+  // If so, rename it.
+  let profD = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile);
+  let blFile = profD.clone();
+  blFile.append(kBlocklist);
+
+  if (blFile.exists()) {
+    gHadBlocklist = true;
+    blFile.moveTo(profD, kBlocklistOld);
+  }
+
+  // Now copy the blocklist from the test to the profile directory
+  let path = os.getFileForPath(__file__);
+  let newBlFile = os.getFileForPath(os.abspath(kNewBlocklistPath, path));
+  newBlFile.copyTo(profD, kBlocklist);
+
+  // Cause a reload of blocklist.xml
+  Services.prefs.setBoolPref(kBlEnabledPref, false);
+  Services.prefs.setBoolPref(kBlEnabledPref, true);
+}
+
+function teardownModule(module) {
+  Services.prefs.setCharPref(kStartPagePref, gOldStartUrl);
+  Services.prefs.setCharPref(kPluginsUpdatePref, gOldPluginUpdateUrl);
+
+  // Remove the blocklist.xml we put into the profile directory.
+  let profD = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile);
+  newBlFile = profD.clone();
+  newBlFile.append("blocklist.xml");
+  newBlFile.remove(false);
+
+  // If there was a blocklist there originally, put it back.
+  if (gHadBlocklist) {
+    let blOldFile = profD.clone();
+    blOldFile.append("blocklist-old.xml");
+    blOldFile.moveTo(profD, "blocklist.xml");
+  }
+
+  // Cause a reload of blocklist.xml
+  Services.prefs.setBoolPref(kBlEnabledPref, false);
+  Services.prefs.setBoolPref(kBlEnabledPref, true);
+}
+
+function test_outdated_plugin_notification() {
+  // Prepare to capture the notification bar
+  NotificationWatcher.planForNotification(mc);
+  let pluginTab = open_content_tab_with_click(mc.menus.helpMenu.whatsNew,
+                                              kPluginUrl);
+  NotificationWatcher.waitForNotification(mc);
+
+  let notificationBar = get_notification_bar_for_tab(mc.tabmail.selectedTab);
+  assert_not_equals(null, notificationBar, "Could not get notification bar");
+  let notifValue = notificationBar.getNotificationWithValue("outdated-plugins");
+  assert_not_equals(null, notifValue, "Notification value was not correct");
+
+  // buttons[0] should be the "update my plugins" button.
+  let buttons = notificationBar.getElementsByTagName("button");
+
+  // Let's make sure that the "update my plugins" button opens up
+  // a tab and takes us to the right place.
+  let updateTab = open_content_tab_with_click(buttons[0], kPluginUpdateUrl);
+  assert_tab_has_title(updateTab, "Plugin Update Page");
+  mc.tabmail.closeTab(updateTab);
+
+  mc.tabmail.closeTab(pluginTab);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/content-tabs/test-plugin-unknown.js
@@ -0,0 +1,137 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mike Conley <mconley@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var MODULE_NAME = 'test-plugin-unknown';
+
+var RELATIVE_ROOT = '../shared-modules';
+var MODULE_REQUIRES = ['folder-display-helpers',
+                       'content-tab-helpers',
+                       'window-helpers'];
+
+var controller = {};
+Components.utils.import('resource://mozmill/modules/controller.js', controller);
+Components.utils.import('resource://gre/modules/Services.jsm');
+var elib = {};
+Components.utils.import('resource://mozmill/modules/elementslib.js', elib);
+
+var gTabmail = null;
+var gContentWindow = null;
+var gJSObject = null;
+var gTabDoc = null;
+var gOldStartPage = null;
+
+const kPluginId = "test-plugin";
+const kStartPagePref = "mailnews.start_page.override_url";
+// RELATIVE_ROOT messes with the collector, so we have to bring the path back
+// so we get the right path for the resources.
+const kUrl = collector.addHttpResource('../content-tabs/html', '');
+const kPluginUrl = kUrl + "unknown-plugin.html";
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let cth = collector.getModule('content-tab-helpers');
+  cth.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+
+  // Set the pref so that what's new opens a local url - we'll save the old
+  // url and put it back in the module teardown.
+  gOldStartPage = Services.prefs.getCharPref(kStartPagePref);
+  Services.prefs.setCharPref(kStartPagePref, kPluginUrl);
+
+  gTabmail = mc.tabmail;
+};
+
+function teardownModule(module) {
+  Services.prefs.setCharPref(kStartPagePref, gOldStartPage);
+}
+
+function openPluginTab() {
+  let tab = open_content_tab_with_click(mc.menus.helpMenu.whatsNew, kPluginUrl);
+  assert_tab_has_title(tab, "Unknown Plugin Test");
+  assert_content_tab_has_url(tab, kPluginUrl);
+
+  gContentWindow = gTabmail.selectedTab.browser.contentWindow;
+  gJSObject = gContentWindow.wrappedJSObject;
+
+  // Strangely, in order to manipulate the embedded plugin,
+  // we have to use getElementById within the context of the
+  // wrappedJSObject of the content tab browser.
+  gTabDoc = gJSObject.window.document;
+}
+
+function closeCurrentTab() {
+  let tab = gTabmail.selectedTab;
+  gTabmail.closeTab(tab);
+}
+
+function test_unknown_plugin_notification_inline() {
+  openPluginTab();
+  let plugin = gTabDoc.getElementById(kPluginId);
+
+  function getStatusDiv() {
+    let submitDiv = gContentWindow
+                    .document
+                    .getAnonymousElementByAttribute(plugin,
+                                                    "class",
+                                                    "installStatus");
+
+    if (!submitDiv)
+      return null;
+
+    return submitDiv;
+  }
+
+  mc.waitFor(function() (getStatusDiv() != null),
+             "Timed out waiting for plugin status div to appear");
+
+  let submitDiv = getStatusDiv();
+  assert_equals("ready", submitDiv.getAttribute("status"),
+                "The plugin install status should have been ready");
+  closeCurrentTab();
+}
+
+function test_unknown_plugin_notification_bar() {
+  // We need to prepare for the notification before the
+  // tab is actually loaded, so we'll close the tab that's
+  // been auto-loaded, and re-open.
+  NotificationWatcher.planForNotification(mc);
+  openPluginTab();
+  NotificationWatcher.waitForNotification(mc);
+  closeCurrentTab();
+}
--- a/mail/test/mozmill/shared-modules/test-content-tab-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-content-tab-helpers.js
@@ -40,16 +40,17 @@ var Cc = Components.classes;
 var Cu = Components.utils;
 
 var elib = {};
 Cu.import('resource://mozmill/modules/elementslib.js', elib);
 var mozmill = {};
 Cu.import('resource://mozmill/modules/mozmill.js', mozmill);
 var utils = {};
 Cu.import('resource://mozmill/modules/utils.js', utils);
+Cu.import("resource://gre/modules/Services.jsm");
 
 const MODULE_NAME = 'content-tab-helpers';
 
 const RELATIVE_ROOT = '../shared-modules';
 
 // we need this for the main controller
 const MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers'];
 
@@ -85,18 +86,56 @@ function installInto(module) {
   module.content_tab_e = content_tab_e;
   module.content_tab_eid = content_tab_eid;
   module.get_content_tab_element_display = get_content_tab_element_display;
   module.assert_content_tab_element_hidden = assert_content_tab_element_hidden;
   module.assert_content_tab_element_visible = assert_content_tab_element_visible;
   module.wait_for_content_tab_element_display_value = wait_for_content_tab_element_display_value;
   module.assert_content_tab_text_present = assert_content_tab_text_present;
   module.assert_content_tab_text_absent = assert_content_tab_text_absent;
+  module.NotificationWatcher = NotificationWatcher;
+  module.get_notification_bar_for_tab = get_notification_bar_for_tab;
+  module.get_test_plugin = get_test_plugin;
+  module.plugins_run_in_separate_processes = plugins_run_in_separate_processes;
 }
 
+/* Allows for planning / capture of notification events within
+ * content tabs, for example: plugin crash notifications, theme
+ * install notifications.
+ */
+const ALERT_TIMEOUT = 10000;
+
+let NotificationWatcher = {
+  planForNotification: function(aController) {
+    this.alerted = false;
+    aController.window.document.addEventListener("AlertActive",
+                                                 this.alertActive, false);
+  },
+  waitForNotification: function(aController) {
+    if (!this.alerted) {
+      aController.waitFor(function () this.alerted, "Timeout waiting for alert",
+                          ALERT_TIMEOUT, 100, this);
+    }
+    // Double check the notification box has finished animating.
+    let notificationBox =
+      mc.tabmail.selectedTab.panel.getElementsByTagName("notificationbox")[0];
+    if (notificationBox && notificationBox._animating)
+      aController.waitFor(function () !notificationBox._animating,
+                          "Timeout waiting for notification box animation to finish",
+                          ALERT_TIMEOUT, 100);
+
+    aController.window.document.removeEventListener("AlertActive",
+                                                    this.alertActive, false);
+  },
+  alerted: false,
+  alertActive: function() {
+    NotificationWatcher.alerted = true;
+  }
+};
+
 /**
  * Opens a content tab with the given URL.
  *
  * @param aURL The URL to load (string).
  * @param [aBackground] Whether the tab is opened in the background. Defaults to
  *                      false.
  * @param [aController] The controller to open the tab in. Defaults to |mc|.
  *
@@ -294,8 +333,66 @@ function assert_content_tab_text_present
  * Asserts that the given text is absent on the content tab's page.
  */
 function assert_content_tab_text_absent(aTab, aText) {
   let html = aTab.browser.contentDocument.documentElement.innerHTML;
   if (html.indexOf(aText) != -1) {
     mark_failure(["Found string \"" + aText + "\" on the content tab's page"]);
   }
 }
+
+/**
+ * Returns the notification bar for a tab if one is currently visible,
+ * null if otherwise.
+ */
+function get_notification_bar_for_tab(aTab) {
+  let notificationBoxEls = mc.tabmail.selectedTab.panel.getElementsByTagName("notificationbox");
+  if (notificationBoxEls.length == 0)
+    return null;
+
+  return notificationBoxEls[0];
+}
+
+/**
+ * Returns the nsIPluginTag for the test plug-in, if it is available.
+ * Returns null otherwise.
+ */
+function get_test_plugin() {
+  var ph = Components.classes["@mozilla.org/plugin/host;1"]
+           .getService(Components.interfaces.nsIPluginHost);
+  var tags = ph.getPluginTags();
+
+  // Find the test plugin
+  for (var i = 0; i < tags.length; i++) {
+    if (tags[i].name == "Test Plug-in")
+      return tags[i];
+  }
+  return null;
+}
+
+/* Returns true if we're currently set up to run plugins in seperate
+ * processes, false otherwise.
+ */
+function plugins_run_in_separate_processes(aController) {
+  let supportsOOPP = false;
+
+  if (aController.mozmillModule.isMac) {
+    if (Services.appinfo.XPCOMABI.match(/x86-/)) {
+      try {
+        supportsOOPP = Services.prefs.getBoolPref("dom.ipc.plugins.enabled.i386.test.plugin");
+      } catch(e) {
+        supportsOOPP = Services.prefs.getBoolPref("dom.ipc.plugins.enabled.i386");
+      }
+    }
+    else if (Services.appinfo.XPCOMABI.match(/x86_64-/)) {
+      try {
+        supportsOOPP = Services.prefs.getBoolPref("dom.ipc.plugins.enabled.x86_64.test.plugin");
+      } catch(e) {
+        supportsOOPP = Services.prefs.getBoolPref("dom.ipc.plugins.enabled.x86_64");
+      }
+    }
+  }
+  else {
+    supportsOOPP = Services.prefs.getBoolPref("dom.ipc.plugins.enabled");
+  }
+
+  return supportsOOPP;
+}