Bug 1322172 - SeaMonkey mail compose should stop being APP_TYPE_EDITOR. r=IanN
authorFrank-Rainer Grahl <frgrahl@gmx.net>
Sun, 30 Jul 2017 21:31:03 +0200
changeset 28520 538e19f624671e70877fb3d2dece742398f94329
parent 28519 5dcb4353fcec7451997c1ccc1364cf27c001b71c
child 28521 6a2729aebec1a43258690cb8aa199b6a92e2da4a
push id1986
push userclokep@gmail.com
push dateWed, 02 Aug 2017 14:43:31 +0000
treeherdercomm-beta@b51c9adf2c9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersIanN
bugs1322172
Bug 1322172 - SeaMonkey mail compose should stop being APP_TYPE_EDITOR. r=IanN
mailnews/compose/src/nsMsgCompose.cpp
mailnews/compose/src/nsMsgSend.cpp
suite/locales/en-US/chrome/mailnews/compose/composeMsgs.properties
suite/mailnews/compose/EdColorPropsOverlay.xul
suite/mailnews/compose/MsgComposeCommands.js
suite/mailnews/compose/messengercompose.xul
suite/mailnews/jar.mn
suite/mailnews/mailWindow.js
--- a/mailnews/compose/src/nsMsgCompose.cpp
+++ b/mailnews/compose/src/nsMsgCompose.cpp
@@ -1027,19 +1027,16 @@ nsMsgCompose::Initialize(nsIMsgComposePa
 
     nsCOMPtr<nsIDocShellTreeItem> treeItem =
       do_QueryInterface(window->GetDocShell());
     nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
     rv = treeItem->GetTreeOwner(getter_AddRefs(treeOwner));
     if (NS_FAILED(rv)) return rv;
 
     m_baseWindow = do_QueryInterface(treeOwner);
-#ifdef MOZ_SUITE
-    window->GetDocShell()->SetAppType(nsIDocShell::APP_TYPE_EDITOR);
-#endif
   }
 
   MSG_ComposeFormat format;
   aParams->GetFormat(&format);
 
   MSG_ComposeType type;
   aParams->GetType(&type);
 
--- a/mailnews/compose/src/nsMsgSend.cpp
+++ b/mailnews/compose/src/nsMsgSend.cpp
@@ -1437,26 +1437,16 @@ nsMsgComposeAndSend::GetEmbeddedObjectIn
   // specified or set to "false".
   if (isData || isNews)
   {
     *acceptObject = mozDoNotSendAttr.IsEmpty() ||
       mozDoNotSendAttr.LowerCaseEqualsLiteral("false");
     return NS_OK;
   }
 
-#ifdef MOZ_SUITE
-  bool isFile =
-    (NS_SUCCEEDED(attachment->m_url->SchemeIs("file", &isFile)) && isFile);
-  if (isFile)
-  {
-    *acceptObject = mozDoNotSendAttr.IsEmpty() ||
-      mozDoNotSendAttr.LowerCaseEqualsLiteral("false");
-    return NS_OK;
-  }
-#endif
   return NS_OK;
 }
 
 
 uint32_t
 nsMsgComposeAndSend::GetMultipartRelatedCount(bool forceToBeCalculated /*=false*/)
 {
   nsresult                  rv = NS_OK;
--- a/suite/locales/en-US/chrome/mailnews/compose/composeMsgs.properties
+++ b/suite/locales/en-US/chrome/mailnews/compose/composeMsgs.properties
@@ -262,8 +262,25 @@ buttonLabelRetry=Retry
 ## %3$S will be replaced with the local folders account name (typically "Local Folders").
 promptToSaveTemplateLocally=Your template was not saved to your templates folder (%1$S) probably because of network errors.\n"Retry" attempts to save again.\n"Save" copies the message to %3$S/%1$S-%2$S and you can continue writing.\n"Cancel" allows you to continue writing without saving your template.
 
 ## LOCALIZATION NOTE(saveToLocalFoldersFailed): Message appears after normal
 ## save fails (e.g., to Sent) and save to Local Folders also fails. This could
 ## occur if network is down and filesystem problems are present such as disk
 ## full, permission issues or hardware failure.
 saveToLocalFoldersFailed=Unable to save your message to local folders. Possibly out of file storage space.
+
+## LOCALIZATION NOTE(blockedAllowResource): %S is the URL to load.
+blockedAllowResource=Unblock %S
+## LOCALIZATION NOTE (blockedContentMessage): Semi-colon list of plural forms.
+## See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+## %S will be replaced by brandShortName.
+## Files must be unblocked individually, therefore the plural form reads:
+## Unblocking a file (one of several) will include it (that one file) in your sent message.
+## In other words:
+## Unblocking one/several file(s) will include it/them in your message.
+blockedContentMessage=%S has blocked a file from loading into this message. Unblocking the file will include it in your sent message.;%S has blocked some files from loading into this message. Unblocking a file will include it in your sent message.
+
+## LOCALIZATION NOTE (blockedContentPrefLabel, blockedContentPrefAccesskey):
+## Same content as (blockedContentPrefLabel, blockedContentPrefAccesskey)
+## in mail directory. SeaMonkey does only use Options and not Preferences.
+blockedContentPrefLabel=Options
+blockedContentPrefAccesskey=O
new file mode 100644
--- /dev/null
+++ b/suite/mailnews/compose/EdColorPropsOverlay.xul
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd" >
+
+<overlay id="mailEdColorPropsOverlay"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript">
+  <![CDATA[
+  function onAccept() {
+    // If it's a file, convert to a data URL.
+    if (gBackgroundImage && /^file:/i.test(gBackgroundImage)) {
+      let nsFile = Services.io.newURI(gBackgroundImage)
+                              .QueryInterface(Components.interfaces.nsIFileURL)
+                              .file;
+      if (nsFile.exists()) {
+        let reader = new FileReader();
+        reader.addEventListener("load", function() {
+          gBackgroundImage = reader.result;
+          gDialog.BackgroundImageInput.value = reader.result;
+          if (onAccept()) {
+            window.close();
+          }
+        });
+        File.createFromNsIFile(nsFile).then(file => {
+          reader.readAsDataURL(file);
+        });
+        return false; // Don't close just yet...
+      }
+    }
+    if (ValidateData()) {
+      // Copy attributes to element we are changing
+      try {
+        GetCurrentEditor().cloneAttributes(gBodyElement, globalElement);
+      } catch (e) {}
+
+      SaveWindowLocation();
+      return true; // do close the window
+    }
+    return false;
+  }
+  ]]>
+  </script>
+
+</overlay>
--- a/suite/mailnews/compose/MsgComposeCommands.js
+++ b/suite/mailnews/compose/MsgComposeCommands.js
@@ -1,14 +1,18 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
 Components.utils.import("resource:///modules/folderUtils.jsm");
 Components.utils.import("resource:///modules/iteratorUtils.jsm");
 Components.utils.import("resource:///modules/mailServices.js");
 
 /**
  * interfaces
  */
 var nsIMsgCompDeliverMode = Components.interfaces.nsIMsgCompDeliverMode;
@@ -45,16 +49,17 @@ var msgWindow = Components.classes["@moz
 var gMessenger = Components.classes["@mozilla.org/messenger;1"]
                            .createInstance(Components.interfaces.nsIMessenger);
 
 /**
  * Global variables, need to be re-initialized every time mostly because we need to release them when the window close
  */
 var gHideMenus;
 var gMsgCompose;
+var gOriginalMsgURI;
 var gAccountManager;
 var gWindowLocked;
 var gContentChanged;
 var gAutoSaving;
 var gCurrentIdentity;
 var defaultSaveOperation;
 var gSendOrSaveOperationInProgress;
 var gCloseWindowAfterSave;
@@ -87,16 +92,17 @@ var gEditingDraft;
 
 var kComposeAttachDirPrefName = "mail.compose.attach.dir";
 
 function InitializeGlobalVariables()
 {
   gAccountManager = Components.classes["@mozilla.org/messenger/account-manager;1"].getService(Components.interfaces.nsIMsgAccountManager);
 
   gMsgCompose = null;
+  gOriginalMsgURI = null;
   gWindowLocked = false;
   gContentChanged = false;
   gCurrentIdentity = null;
   defaultSaveOperation = "draft";
   gSendOrSaveOperationInProgress = false;
   gAutoSaving = false;
   gCloseWindowAfterSave = false;
   gSavedSendNowKey = null;
@@ -118,16 +124,17 @@ function InitializeGlobalVariables()
 InitializeGlobalVariables();
 
 function ReleaseGlobalVariables()
 {
   gAccountManager = null;
   gCurrentIdentity = null;
   gCharsetConvertManager = null;
   gMsgCompose = null;
+  gOriginalMsgURI = null;
   gMailSession = null;
 }
 
 function disableEditableFields()
 {
   gMsgCompose.editor.flags |= nsIPlaintextEditorMail.eEditorReadonlyMask;
   var disableElements = document.getElementsByAttribute("disableonsend", "true");
   for (let i = 0; i < disableElements.length; i++)
@@ -982,16 +989,147 @@ function handleMailtoArgs(mailtoUrl)
     var uri = Services.io.newURI(mailtoUrl);
 
     if (uri)
       return sMsgComposeService.getParamsForMailto(uri);
   }
 
   return null;
 }
+/**
+ * Handle ESC keypress from composition window for
+ * notifications with close button in the
+ * attachmentNotificationBox.
+ */
+function handleEsc()
+{
+  let activeElement = document.activeElement;
+
+  // If findbar is visible and the focus is in the message body,
+  // hide it. (Focus on the findbar is handled by findbar itself).
+  let findbar = document.getElementById("FindToolbar");
+  if (findbar && !findbar.hidden && activeElement.id == "content-frame") {
+    findbar.close();
+    return;
+  }
+
+  // If there is a notification in the attachmentNotificationBox
+  // AND focus is in message body, subject field or on the notification,
+  // hide it.
+  let notification = document.getElementById("attachmentNotificationBox")
+                             .currentNotification;
+  if (notification && (activeElement.id == "content-frame" ||
+      activeElement.parentNode.parentNode.id == "msgSubject" ||
+      notification.contains(activeElement) ||
+      activeElement.classList.contains("messageCloseButton"))) {
+    notification.close();
+  }
+}
+
+/**
+ * On paste or drop, we may want to modify the content before inserting it into
+ * the editor, replacing file URLs with data URLs when appropriate.
+ */
+function onPasteOrDrop(e) {
+  // For paste use e.clipboardData, for drop use e.dataTransfer.
+  let dataTransfer = ("clipboardData" in e) ? e.clipboardData : e.dataTransfer;
+
+  if (!dataTransfer.types.includes("text/html")) {
+    return;
+  }
+
+  if (!gMsgCompose.composeHTML) {
+    // We're in the plain text editor. Nothing to do here.
+    return;
+  }
+
+  let html = dataTransfer.getData("text/html");
+  let doc = (new DOMParser()).parseFromString(html, "text/html");
+  let tmpD = Services.dirsvc.get("TmpD", Components.interfaces.nsIFile);
+  let pendingConversions = 0;
+  let needToPreventDefault = true;
+  for (let img of doc.images) {
+    if (!/^file:/i.test(img.src)) {
+      // Doesn't start with file:. Nothing to do here.
+      continue;
+    }
+
+    // This may throw if the URL is invalid for the OS.
+    let nsFile;
+    try {
+      nsFile = Services.io.getProtocolHandler("file")
+                 .QueryInterface(Components.interfaces.nsIFileProtocolHandler)
+                 .getFileFromURLSpec(img.src);
+    } catch (ex) {
+      continue;
+    }
+
+    if (!nsFile.exists()) {
+      continue;
+    }
+
+    if (!tmpD.contains(nsFile)) {
+       // Not anywhere under the temp dir.
+      continue;
+    }
+
+    let contentType = Components.classes["@mozilla.org/mime;1"]
+                        .getService(Components.interfaces.nsIMIMEService)
+                        .getTypeFromFile(nsFile);
+    if (!contentType.startsWith("image/")) {
+      continue;
+    }
+
+    // If we ever get here, we need to prevent the default paste or drop since
+    // the code below will do its own insertion.
+    if (needToPreventDefault) {
+      e.preventDefault();
+      needToPreventDefault = false;
+    }
+
+    File.createFromNsIFile(nsFile).then(function(file) {
+      if (file.lastModified < (Date.now() - 60000)) {
+        // Not put in temp in the last minute. May be something other than
+        // a copy-paste. Let's not allow that.
+        return;
+      }
+
+      let doTheInsert = function() {
+        // Now run it through sanitation to make sure there wasn't any
+        // unwanted things in the content.
+        let ParserUtils = Components.classes["@mozilla.org/parserutils;1"]
+                            .getService(Components.interfaces.nsIParserUtils);
+        let html2 = ParserUtils.sanitize(doc.documentElement.innerHTML,
+                                         ParserUtils.SanitizerAllowStyle);
+        getBrowser().contentDocument.execCommand("insertHTML", false, html2);
+      }
+
+      // Everything checks out. Convert file to data URL.
+      let reader = new FileReader();
+      reader.addEventListener("load", function() {
+        let dataURL = reader.result;
+        pendingConversions--;
+        img.src = dataURL;
+        if (pendingConversions == 0) {
+          doTheInsert();
+        }
+      });
+
+      reader.addEventListener("error", function() {
+        pendingConversions--;
+        if (pendingConversions == 0) {
+          doTheInsert();
+        }
+      });
+
+      pendingConversions++;
+      reader.readAsDataURL(file);
+    });
+  }
+}
 
 function ComposeStartup(aParams)
 {
   var params = null; // New way to pass parameters to the compose window as a nsIMsgComposeParameters object
   var args = null;   // old way, parameters are passed as a string
 
   if (aParams)
     params = aParams;
@@ -1013,16 +1151,19 @@ function ComposeStartup(aParams)
   }
 
   // Set the document language to the preference as early as possible.
   document.documentElement
           .setAttribute("lang", getPref("spellchecker.dictionary"));
 
   var identityList = GetMsgIdentityElement();
 
+  document.addEventListener("paste", onPasteOrDrop, false);
+  document.addEventListener("drop", onPasteOrDrop, false);
+
   if (identityList)
     FillIdentityList(identityList);
 
   if (!params) {
     // This code will go away soon as now arguments are passed to the window using a object of type nsMsgComposeParams instead of a string
 
     params = Components.classes["@mozilla.org/messengercompose/composeparams;1"].createInstance(Components.interfaces.nsIMsgComposeParams);
     params.composeFields = Components.classes["@mozilla.org/messengercompose/composefields;1"].createInstance(Components.interfaces.nsIMsgCompFields);
@@ -1106,16 +1247,23 @@ function ComposeStartup(aParams)
     identityList.getElementsByAttribute("identitykey", params.identity.key)[0];
   if (params.composeFields.from)
     identityList.value = MailServices.headerParser.parseDecodedHeader(params.composeFields.from)[0].toString();
   LoadIdentity(true);
   if (sMsgComposeService)
   {
     // Get the <editor> element to startup an editor
     var editorElement = GetCurrentEditorElement();
+
+    // Remember the original message URI. When editing a draft which is a reply
+    // or forwarded message, this gets overwritten by the ancestor's message URI so
+    // the disposition flags ("replied" or "forwarded") can be set on the ancestor.
+    // For our purposes we need the URI of the message being processed, not its
+    // original ancestor.
+    gOriginalMsgURI = params.originalMsgURI;
     gMsgCompose = sMsgComposeService.initCompose(params, window,
                                                  editorElement.docShell);
     if (gMsgCompose)
     {
       if (!editorElement)
       {
         dump("Failed to get editor element!\n");
         return;
@@ -3144,16 +3292,98 @@ function InitEditor(editor)
   gMsgCompose.initEditor(editor, window.content);
   InlineSpellCheckerUI.init(editor);
   EnableInlineSpellCheck(getPref("mail.spellcheck.inline"));
   document.getElementById("menu_inlineSpellCheck").setAttribute("disabled", !InlineSpellCheckerUI.canSpellCheck);
 
   // Listen for spellchecker changes, set the document language to the
   // dictionary picked by the user via the right-click menu in the editor.
   document.addEventListener("spellcheck-changed", updateDocumentLanguage);
+
+  // XXX: the error event fires twice for each load. Why??
+  editor.document.body.addEventListener("error", function(event) {
+    if (event.target.localName != "img") {
+      return;
+    }
+
+    if (event.target.getAttribute("moz-do-not-send") == "true") {
+      return;
+    }
+
+    let src = event.target.src;
+
+    if (!src) {
+      return;
+    }
+
+    if (!/^file:/i.test(src)) {
+      // Check if this is a protocol that can fetch parts.
+      let protocol = src.substr(0, src.indexOf(":")).toLowerCase();
+      if (!(Services.io.getProtocolHandler(protocol) instanceof
+            Components.interfaces.nsIMsgMessageFetchPartService)) {
+        // Can't fetch parts, don't try to load.
+        return;
+      }
+    }
+
+    if (event.target.classList.contains("loading-internal")) {
+      // We're already loading this, or tried so unsuccesfully.
+      return;
+    }
+
+    if (gOriginalMsgURI) {
+      let msgSvc = Components.classes["@mozilla.org/messenger;1"]
+                     .createInstance(Components.interfaces.nsIMessenger)
+                     .messageServiceFromURI(gOriginalMsgURI);
+      let originalMsgNeckoURI = {};
+      msgSvc.GetUrlForUri(gOriginalMsgURI, originalMsgNeckoURI, null);
+
+      if (src.startsWith(originalMsgNeckoURI.value.spec)) {
+        // Reply/Forward/Edit Draft/Edit as New can contain references to
+        // images in the original message. Load those and make them data: URLs
+        // now.
+        event.target.classList.add("loading-internal");
+        try {
+          loadBlockedImage(src);
+        } catch (e) {
+          // Couldn't load the referenced image.
+          Components.utils.reportError(e);
+        }
+      }
+      else {
+        // Appears to reference a random message. Notify and keep blocking.
+        gComposeNotificationBar.setBlockedContent(src);
+      }
+    }
+    else {
+      // For file:, and references to parts of random messages, show the
+      // blocked content notification.
+      gComposeNotificationBar.setBlockedContent(src);
+    }
+  }, true);
+
+  // Convert mailnews URL back to data: URL.
+  let background = editor.document.body.background;
+  if (background && gOriginalMsgURI) {
+    // Check that background has the same URL as the message itself.
+    let msgSvc = Components.classes["@mozilla.org/messenger;1"]
+                   .createInstance(Components.interfaces.nsIMessenger)
+                   .messageServiceFromURI(gOriginalMsgURI);
+    let originalMsgNeckoURI = {};
+    msgSvc.GetUrlForUri(gOriginalMsgURI, originalMsgNeckoURI, null);
+
+    if (background.startsWith(originalMsgNeckoURI.value.spec)) {
+      try {
+        editor.document.body.background = loadBlockedImage(background, true);
+      } catch (e) {
+        // Couldn't load the referenced image.
+        Components.utils.reportError(e);
+      }
+    }
+  }
 }
 
 /**
  * The event listener for the "spellcheck-changed" event updates
  * the document language.
  */
 function updateDocumentLanguage(event)
 {
@@ -3210,8 +3440,203 @@ function getPref(aPrefName, aIsComplex)
     case Components.interfaces.nsIPrefBranch.PREF_INT:
       return Services.prefs.getIntPref(aPrefName);
     case Components.interfaces.nsIPrefBranch.PREF_STRING:
       return Services.prefs.getCharPref(aPrefName);
     default: // includes nsIPrefBranch.PREF_INVALID
       return null;
   }
 }
+
+/**
+ * Object to handle message related notifications that are showing in a
+ * notificationbox below the composed message content.
+ */
+var gComposeNotificationBar = {
+
+  get notificationBar() {
+    delete this.notificationBar;
+    return this.notificationBar = document.getElementById("attachmentNotificationBox");
+  },
+
+  setBlockedContent: function(aBlockedURI) {
+    let brandName = sBrandBundle.getString("brandShortName");
+    let buttonLabel = sComposeMsgsBundle.getString("blockedContentPrefLabel");
+    let buttonAccesskey = sComposeMsgsBundle.getString("blockedContentPrefAccesskey");
+
+    let buttons = [{
+      label: buttonLabel,
+      accessKey: buttonAccesskey,
+      popup: "blockedContentOptions",
+      callback: function(aNotification, aButton) {
+        return true; // keep notification open
+      }
+    }];
+
+    // The popup value is a space separated list of all the blocked urls.
+    let popup = document.getElementById("blockedContentOptions");
+    let urls = popup.value ? popup.value.split(" ") : [];
+    if (!urls.includes(aBlockedURI)) {
+      urls.push(aBlockedURI);
+    }
+    popup.value = urls.join(" ");
+
+    let msg = sComposeMsgsBundle.getFormattedString("blockedContentMessage",
+                                                    [brandName, brandName]);
+    msg = PluralForm.get(urls.length, msg);
+
+    if (!this.isShowingBlockedContentNotification()) {
+      this.notificationBar
+          .appendNotification(msg, "blockedContent", null,
+                              this.notificationBar.PRIORITY_WARNING_MEDIUM,
+                              buttons);
+    }
+    else {
+      this.notificationBar.getNotificationWithValue("blockedContent")
+                          .setAttribute("label", msg);
+    }
+  },
+
+  isShowingBlockedContentNotification: function() {
+    return !!this.notificationBar.getNotificationWithValue("blockedContent");
+  },
+
+  clearNotifications: function(aValue) {
+    this.notificationBar.removeAllNotifications(true);
+  }
+};
+
+/**
+ * Populate the menuitems of what blocked content to unblock.
+ */
+function onBlockedContentOptionsShowing(aEvent) {
+  let urls = aEvent.target.value ? aEvent.target.value.split(" ") : [];
+
+  // Out with the old...
+  let childNodes = aEvent.target.childNodes;
+  for (let i = childNodes.length - 1; i >= 0; i--) {
+    childNodes[i].remove();
+  }
+
+  // ... and in with the new.
+  for (let url of urls) {
+    let menuitem = document.createElement("menuitem");
+    let fString = sComposeMsgsBundle.getFormattedString("blockedAllowResource",
+                                                        [url]);
+    menuitem.setAttribute("label", fString);
+    menuitem.setAttribute("crop", "center");
+    menuitem.setAttribute("value", url);
+    menuitem.setAttribute("oncommand",
+                          "onUnblockResource(this.value, this.parentNode);");
+    aEvent.target.appendChild(menuitem);
+  }
+}
+
+/**
+ * Handle clicking the "Load <url>" in the blocked content notification bar.
+ * @param {String} aURL - the URL that was unblocked
+ * @param {Node} aNode  - the node holding as value the URLs of the blocked
+ *                        resources in the message (space separated).
+ */
+function onUnblockResource(aURL, aNode) {
+  try {
+    loadBlockedImage(aURL);
+  } catch (e) {
+    // Couldn't load the referenced image.
+    Components.utils.reportError(e);
+  } finally {
+    // Remove it from the list on success and failure.
+    let urls = aNode.value.split(" ");
+    for (let i = 0; i < urls.length; i++) {
+      if (urls[i] == aURL) {
+        urls.splice(i, 1);
+        aNode.value = urls.join(" ");
+        if (urls.length == 0) {
+          gComposeNotificationBar.clearNotifications();
+        }
+        break;
+      }
+    }
+  }
+}
+
+/**
+ * Convert the blocked content to a data URL and swap the src to that for the
+ * elements that were using it.
+ *
+ * @param {String}  aURL - (necko) URL to unblock
+ * @param {Bool}    aReturnDataURL - return data: URL instead of processing image
+ * @return {String} the image as data: URL.
+ * @throw Error()   if reading the data failed
+ */
+function loadBlockedImage(aURL, aReturnDataURL = false) {
+  let filename;
+  if (/^(file|chrome):/i.test(aURL)) {
+    filename = aURL.substr(aURL.lastIndexOf("/") + 1);
+  }
+  else {
+    let fnMatch = /[?&;]filename=([^?&]+)/.exec(aURL);
+    filename = (fnMatch && fnMatch[1]) || "";
+  }
+
+  filename = decodeURIComponent(filename);
+  let uri = Services.io.newURI(aURL);
+  let contentType;
+  if (filename) {
+    try {
+      contentType = Components.classes["@mozilla.org/mime;1"]
+                      .getService(Components.interfaces.nsIMIMEService)
+                      .getTypeFromURI(uri);
+    } catch (ex) {
+      contentType = "image/png";
+    }
+
+    if (!contentType.startsWith("image/")) {
+      // Unsafe to unblock this. It would just be garbage either way.
+      throw new Error("Won't unblock; URL=" + aURL +
+                      ", contentType=" + contentType);
+    }
+  }
+  else {
+    // Assuming image/png is the best we can do.
+    contentType = "image/png";
+  }
+
+  let channel =
+    Services.io.newChannelFromURI2(uri,
+      null,
+      Services.scriptSecurityManager.getSystemPrincipal(),
+      null,
+      Components.interfaces.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+      Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+
+  let inputStream = channel.open();
+  let stream = Components.classes["@mozilla.org/binaryinputstream;1"]
+                 .createInstance(Components.interfaces.nsIBinaryInputStream);
+  stream.setInputStream(inputStream);
+  let streamData = "";
+  try {
+    while (stream.available() > 0) {
+      streamData += stream.readBytes(stream.available());
+    }
+  } catch(e) {
+    stream.close();
+    throw new Error("Couln't read all data from URL=" + aURL + " (" + e +")");
+  }
+  stream.close();
+
+  let encoded = btoa(streamData);
+  let dataURL = "data:" + contentType +
+                (filename ? ";filename=" + encodeURIComponent(filename) : "") +
+                ";base64," + encoded;
+
+  if (aReturnDataURL) {
+    return dataURL;
+  }
+
+  let editor = GetCurrentEditor();
+  for (let img of editor.document.images) {
+    if (img.src == aURL) {
+      img.src = dataURL; // Swap to data URL.
+      img.classList.remove("loading-internal");
+    }
+  }
+}
--- a/suite/mailnews/compose/messengercompose.xul
+++ b/suite/mailnews/compose/messengercompose.xul
@@ -142,16 +142,17 @@
   <key id="showHideSidebar"/>
   <!-- Tab/F6 Keys -->
   <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control"/>
   <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/>
   <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control"/>
   <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/>
   <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="shift"/>
   <key keycode="VK_F6" oncommand="SwitchElementFocus(event);"/>
+  <key keycode="VK_ESCAPE" oncommand="handleEsc();"/>
 </keyset>
 <keyset id="editorKeys"/>
 <keyset id="composeKeys">
   <key id="key_renameAttachment"/>
 </keyset>
 
   <popupset id="contentAreaContextSet"/>
 
@@ -179,16 +180,20 @@
                 accesskey="&attachFile.accesskey;"
                 command="cmd_attachFile"/>
       <menuitem label="&attachPage.label;"
                 accesskey="&attachPage.accesskey;"
                 command="cmd_attachPage"/>
     </menupopup>
   </popupset>
 
+  <menupopup id="blockedContentOptions" value=""
+             onpopupshowing="onBlockedContentOptionsShowing(event);">
+  </menupopup>
+
   <vbox id="titlebar"/>
 
   <toolbox id="compose-toolbox"
            class="toolbox-top"
            mode="full"
            defaultmode="full">
     <toolbar id="compose-toolbar-menubar2"
              type="menubar"
@@ -643,16 +648,23 @@
             name="browser.message.body"
             minheight="100"
             flex="1"
             ondblclick="EditorDblClick(event);"
             context="contentAreaContextMenu"/>
   </vbox>
 </hbox>
 
+  <hbox>
+    <notificationbox id="attachmentNotificationBox"
+                     flex="1"
+                     notificationside="bottom"/>
+  </hbox>
+
+
   <statusbar id="status-bar" class="chromeclass-status">
     <statusbarpanel id="component-bar"/>
     <statusbarpanel id="statusText" flex="1"/>
     <statusbarpanel class="statusbarpanel-progress" id="statusbar-progresspanel" collapsed="true">
       <progressmeter id="compose-progressmeter" class="progressmeter-statusbar" mode="normal" value="0"/>
     </statusbarpanel>
     <statusbarpanel checkfunc="MailCheckBeforeOfflineChange()" id="offline-status" class="statusbarpanel-iconic"/>
   </statusbar>
--- a/suite/mailnews/jar.mn
+++ b/suite/mailnews/jar.mn
@@ -13,16 +13,17 @@ messenger.jar:
 % content messenger-region %content/messenger-region/
 % overlay chrome://communicator/content/pref/preferences.xul                   chrome://messenger/content/mailPrefsOverlay.xul
 % overlay chrome://communicator/content/pref/pref-appearance.xul               chrome://messenger/content/mailPrefsOverlay.xul
 % overlay chrome://communicator/content/pref/pref-scripts.xul                  chrome://messenger/content/mailPrefsOverlay.xul
 % overlay chrome://communicator/content/pref/pref-cookies.xul                  chrome://messenger/content/mailPrefsOverlay.xul
 % overlay chrome://editor/content/editorTasksOverlay.xul                       chrome://messenger/content/mailTasksOverlay.xul
 % overlay chrome://messenger/content/addressbook/abSelectAddressesDialog.xul   chrome://messenger/content/mailOverlay.xul
 % overlay chrome://editor/content/composerOverlay.xul                          chrome://messenger/content/mailEditorOverlay.xul
+% overlay chrome://editor/content/EdColorProps.xul                             chrome://messenger/content/messengercompose/EdColorPropsOverlay.xul
 % overlay chrome://editor/content/EdImageOverlay.xul                           chrome://messenger/content/messengercompose/mailComposeEditorOverlay.xul
 % overlay chrome://editor/content/EdLinkProps.xul                              chrome://messenger/content/messengercompose/mailComposeEditorOverlay.xul
     content/messenger/browserRequest.xul
     content/messenger/browserRequest.js
     content/messenger/msgViewPickerOverlay.js
     content/messenger/mailViewSetup.js
     content/messenger/mailViewSetup.xul
     content/messenger/mailViewList.xul
@@ -90,16 +91,17 @@ messenger.jar:
     content/messenger/ABSearchDialog.js                                        (search/ABSearchDialog.js)
     content/messenger/FilterListDialog.xul                                     (search/FilterListDialog.xul)
     content/messenger/FilterListDialog.js                                      (search/FilterListDialog.js)
     content/messenger/messengercompose/pref-composing_messages.xul             (compose/prefs/pref-composing_messages.xul)
     content/messenger/messengercompose/pref-composing_messages.js              (compose/prefs/pref-composing_messages.js)
     content/messenger/messengercompose/pref-formatting.xul                     (compose/prefs/pref-formatting.xul)
     content/messenger/messengercompose/pref-formatting.js                      (compose/prefs/pref-formatting.js)
     content/messenger/messengercompose/messengercompose.xul                    (compose/messengercompose.xul)
+    content/messenger/messengercompose/EdColorPropsOverlay.xul                 (compose/EdColorPropsOverlay.xul)
     content/messenger/messengercompose/mailComposeOverlay.xul                  (compose/mailComposeOverlay.xul)
     content/messenger/messengercompose/msgComposeContextOverlay.xul            (compose/msgComposeContextOverlay.xul)
     content/messenger/messengercompose/MsgComposeCommands.js                   (compose/MsgComposeCommands.js)
     content/messenger/messengercompose/addressingWidgetOverlay.js              (compose/addressingWidgetOverlay.js)
     content/messenger/messengercompose/addressingWidgetOverlay.xul             (compose/addressingWidgetOverlay.xul)
     content/messenger/messengercompose/mailComposeExtrasOverlay.xul            (compose/mailComposeExtrasOverlay.xul)
     content/messenger/addressbook/addressbook.js                               (addrbook/addressbook.js)
     content/messenger/addressbook/addressbook.xul                              (addrbook/addressbook.xul)
--- a/suite/mailnews/mailWindow.js
+++ b/suite/mailnews/mailWindow.js
@@ -56,16 +56,109 @@ function OnMailWindowUnload()
   if (mailSession instanceof Components.interfaces.nsIMsgMailSession)
     mailSession.RemoveFolderListener(folderListener);
   mailSession.RemoveMsgWindow(msgWindow);
   messenger.setWindow(null, null);
 
   msgWindow.closeWindow();
 }
 
+/**
+ * When copying/dragging, convert imap/mailbox URLs of images into data URLs so
+ * that the images can be accessed in a paste elsewhere.
+ */
+function onCopyOrDragStart(e) {
+  let sourceDoc = getBrowser().contentDocument;
+  if (e.target.ownerDocument != sourceDoc) {
+    // We're only interested if this is in the message content.
+    return; 
+  }
+
+  let imgMap = new Map(); // Mapping img.src -> dataURL.
+
+  // For copy, the data of what is to be copied is not accessible at this point.
+  // Figure out what images are a) part of the selection and b) visible in
+  // the current document. If their source isn't http or data already, convert
+  // them to data URLs.
+  let selection = sourceDoc.getSelection();
+  let draggedImg = selection.isCollapsed ? e.target : null;
+  for (let img of sourceDoc.images) {
+    if (/^(https?|data):/.test(img.src)) {
+      continue;
+    }
+
+    if (img.naturalWidth == 0) {
+      // Broken/inaccessible image then...
+      continue;
+    }
+
+    if (!draggedImg && !selection.containsNode(img, true)) {
+      continue;
+    }
+
+    let style = window.getComputedStyle(img);
+    if (style.display == "none" || style.visibility == "hidden") {
+      continue;
+    }
+
+    // Do not convert if the image is specifically flagged to not snarf.
+    if (img.getAttribute("moz-do-not-send") == "true") {
+      continue;
+    }
+
+    // We don't need to wait for the image to load. If it isn't already loaded
+    // in the source document, we wouldn't want it anyway.
+    let canvas = sourceDoc.createElement("canvas");
+    canvas.width = img.width;
+    canvas.height = img.height;
+    canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
+
+    let type = /\.jpe?g$/i.test(img.src) ? "image/jpg" : "image/png";
+    imgMap.set(img.src, canvas.toDataURL(type));
+  }
+
+  if (imgMap.size == 0) {
+    // Nothing that needs converting!
+    return;
+  }
+
+  let clonedSelection = draggedImg ? draggedImg.cloneNode(false) :
+                                     selection.getRangeAt(0).cloneContents();
+  let div = sourceDoc.createElement("div");
+  div.appendChild(clonedSelection);
+
+  let images = div.querySelectorAll("img");
+  for (let img of images) {
+    if (!imgMap.has(img.src)) {
+      continue;
+    }
+    img.src = imgMap.get(img.src);
+  }
+
+  let html = div.innerHTML;
+  let parserUtils = Components.classes["@mozilla.org/parserutils;1"]
+                      .getService(Components.interfaces.nsIParserUtils);
+  let plain = 
+    parserUtils.convertToPlainText(html,
+      Components.interfaces.nsIDocumentEncoder.OutputForPlainTextClipboardCopy,
+      0);
+      
+  // Copy operation.
+  if ("clipboardData" in e) { 
+    e.clipboardData.setData("text/html", html);
+    e.clipboardData.setData("text/plain", plain);
+    e.preventDefault();
+  }
+  // Drag operation.
+  else if ("dataTransfer" in e) { 
+    e.dataTransfer.setData("text/html", html);
+    e.dataTransfer.setData("text/plain", plain);
+  }
+}
+
 function CreateMailWindowGlobals()
 {
   // get the messenger instance
   messenger = Components.classes["@mozilla.org/messenger;1"]
                         .createInstance(Components.interfaces.nsIMessenger);
 
   //Create windows status feedback
   // set the JS implementation of status feedback before creating the c++ one..
@@ -127,16 +220,19 @@ function InitMsgWindow()
 
   var messagepane = getMessageBrowser();
   messagepane.docShell.allowAuth = false;
   messagepane.docShell.allowDNSPrefetch = false;
   msgWindow.rootDocShell.allowAuth = true;
   msgWindow.rootDocShell.appType = Components.interfaces.nsIDocShell.APP_TYPE_MAIL;
   // Ensure we don't load xul error pages into the main window
   msgWindow.rootDocShell.useErrorPages = false;
+
+  document.addEventListener("copy", onCopyOrDragStart, true);
+  document.addEventListener("dragstart", onCopyOrDragStart, true);
 }
 
 function messagePaneOnResize(event)
 {
   // scale any overflowing images
   var messagepane = getMessageBrowser();
   var doc = messagepane.contentDocument;
   var imgs = doc.images;