Files
binoc-central-mirror/mail/base/content/mailWindow.js
T
2020-05-10 13:52:36 -04:00

731 lines
23 KiB
JavaScript

/** ***** BEGIN LICENSE BLOCK *****
* 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:///modules/mailServices.js");
Components.utils.import("resource:///modules/appIdleManager.js");
Components.utils.import("resource:///modules/MailUtils.js");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource:///modules/gloda/log4moz.js");
Components.utils.import("resource:///modules/gloda/public.js");
//This file stores variables common to mail windows
var messenger;
var statusFeedback;
var msgWindow;
var accountManager;
var gContextMenu;
var gMailWindowLog = Log4Moz.getConfiguredLogger("mailWindow", Log4Moz.Level.Debug, Log4Moz.Level.Debug, Log4Moz.Level.Debug);
/**
* Called by messageWindow.xul:onunload, the 'single message display window'.
*
* Also called by messenger.xul:onunload's (the 3-pane window inside of tabs
* window) unload function, OnUnloadMessenger.
*/
function OnMailWindowUnload()
{
MailOfflineMgr.uninit();
ClearPendingReadTimer();
// all dbview closing is handled by OnUnloadMessenger for the 3-pane (it closes
// the tabs which close their views) and OnUnloadMessageWindow for the
// standalone message window.
MailServices.mailSession.RemoveMsgWindow(msgWindow);
// the tabs have the FolderDisplayWidget close their 'messenger' instances for us
window.QueryInterface(Components.interfaces.nsIDOMChromeWindow)
.browserDOMWindow = null;
msgWindow.closeWindow();
msgWindow.msgHeaderSink = null;
msgWindow.notificationCallbacks = null;
gDBView = null;
window.MsgStatusFeedback.unload();
Components.classes["@mozilla.org/activity-manager;1"]
.getService(Components.interfaces.nsIActivityManager)
.removeListener(window.MsgStatusFeedback);
}
/**
* 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 browser = getBrowser();
if (!browser) {
// We don't care if this isn't coming from a browser
return;
}
let sourceDoc = browser.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);
if ("clipboardData" in e) { // copy
e.clipboardData.setData("text/html", html);
e.clipboardData.setData("text/plain", plain);
e.preventDefault();
}
else if ("dataTransfer" in e) { // drag
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);
window.addEventListener("blur", appIdleManager.onBlur, false);
window.addEventListener("focus", appIdleManager.onFocus, false);
//Create windows status feedback
// set the JS implementation of status feedback before creating the c++ one..
window.MsgStatusFeedback = new nsMsgStatusFeedback();
// double register the status feedback object as the xul browser window implementation
window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsIDocShellTreeItem).treeOwner
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIXULWindow)
.XULBrowserWindow = window.MsgStatusFeedback;
window.QueryInterface(Components.interfaces.nsIDOMChromeWindow)
.browserDOMWindow = new nsBrowserAccess();
statusFeedback = Components.classes["@mozilla.org/messenger/statusfeedback;1"]
.createInstance(Components.interfaces.nsIMsgStatusFeedback);
statusFeedback.setWrappedStatusFeedback(window.MsgStatusFeedback);
Components.classes["@mozilla.org/activity-manager;1"]
.getService(Components.interfaces.nsIActivityManager)
.addListener(window.MsgStatusFeedback);
//Create message window object
msgWindow = Components.classes["@mozilla.org/messenger/msgwindow;1"]
.createInstance(Components.interfaces.nsIMsgWindow);
accountManager = MailServices.accounts;
msgWindow.notificationCallbacks = new BadCertHandler();
}
function InitMsgWindow()
{
msgWindow.windowCommands = new nsMsgWindowCommands();
// set the domWindow before setting the status feedback and header sink objects
msgWindow.domWindow = window;
msgWindow.statusFeedback = statusFeedback;
msgWindow.msgHeaderSink = messageHeaderSink;
MailServices.mailSession.AddMsgWindow(msgWindow);
let messagepane = document.getElementById("messagepane");
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);
}
// We're going to implement our status feedback for the mail window in JS now.
// the following contains the implementation of our status feedback object
function nsMsgStatusFeedback()
{
this._statusText = document.getElementById("statusText");
this._progressBar = document.getElementById("statusbar-icon");
this._progressBarContainer = document.getElementById("statusbar-progresspanel");
this._throbber = document.getElementById("throbber-box");
this._activeProcesses = new Array();
// make sure the stop button is accurate from the get-go
goUpdateCommand("cmd_stop");
}
nsMsgStatusFeedback.prototype =
{
// Document elements.
_statusText: null,
_progressBar: null,
_progressBarContainer: null,
_throbber: null,
// Member variables.
_startTimeoutID: null,
_stopTimeoutID: null,
// How many start meteors have been requested.
_startRequests: 0,
_meteorsSpinning: false,
_defaultStatusText: null,
_progressBarVisible: false,
_activeProcesses: null,
_statusFeedbackProgress: -1,
// unload - call to remove links to listeners etc.
unload: function () {
// Remove listeners for any active processes we have hooked ourselves into.
this._activeProcesses.forEach(function (element) {
element.removeListener(this);
}, this);
},
// nsIXULBrowserWindow implementation.
setJSStatus: function(status) {
if (status.length > 0)
this.showStatusString(status);
},
setOverLink: function(link, context) {
this._statusText.label = link;
},
// Called before links are navigated to to allow us to retarget them if needed.
onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
return originalTarget;
},
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsIMsgStatusFeedback) ||
iid.equals(Components.interfaces.nsIXULBrowserWindow) ||
iid.equals(Components.interfaces.nsIActivityMgrListener) ||
iid.equals(Components.interfaces.nsIActivityListener) ||
iid.equals(Components.interfaces.nsISupportsWeakReference) ||
iid.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_NOINTERFACE;
},
// nsIMsgStatusFeedback implementation.
showStatusString: function(statusText) {
if (!statusText)
statusText = this._defaultStatusText;
else
this._defaultStatusText = "";
this._statusText.label = statusText;
},
setStatusString: function(status) {
if (status.length > 0) {
this._defaultStatusText = status;
this._statusText.label = status;
}
},
_startMeteors: function() {
this._meteorsSpinning = true;
this._startTimeoutID = null;
// Turn progress meter on.
this.updateProgress();
// Start the throbber.
if (this._throbber)
this._throbber.setAttribute("busy", true);
// Update the stop button
goUpdateCommand("cmd_stop");
},
startMeteors: function() {
this._startRequests++;
// If we don't already have a start meteor timeout pending
// and the meteors aren't spinning, then kick off a start.
if (!this._startTimeoutID && !this._meteorsSpinning &&
"MsgStatusFeedback" in window)
this._startTimeoutID =
setTimeout('window.MsgStatusFeedback._startMeteors();', 500);
// Since we are going to start up the throbber no sense in processing
// a stop timeout...
if (this._stopTimeoutID) {
clearTimeout(this._stopTimeoutID);
this._stopTimeoutID = null;
}
},
_stopMeteors: function() {
this.showStatusString(this._defaultStatusText);
// stop the throbber
if (this._throbber)
this._throbber.setAttribute("busy", false);
this._meteorsSpinning = false;
this._stopTimeoutID = null;
// Turn progress meter off.
this._statusFeedbackProgress = -1;
this.updateProgress();
// Update the stop button
goUpdateCommand("cmd_stop");
},
stopMeteors: function() {
if (this._startRequests > 0)
this._startRequests--;
// If we are going to be starting the meteors, cancel the start.
if (this._startRequests == 0 && this._startTimeoutID) {
clearTimeout(this._startTimeoutID);
this._startTimeoutID = null;
}
// If we have no more pending starts and we don't have a stop timeout
// already in progress AND the meteors are currently running then fire a
// stop timeout to shut them down.
if (this._startRequests == 0 && !this._stopTimeoutID &&
this._meteorsSpinning && "MsgStatusFeedback" in window) {
this._stopTimeoutID =
setTimeout('window.MsgStatusFeedback._stopMeteors();', 500);
}
},
showProgress: function(percentage) {
this._statusFeedbackProgress = percentage;
this.updateProgress();
},
updateProgress: function() {
if (this._meteorsSpinning) {
// In this function, we expect that the maximum for each progress is 100,
// i.e. we are dealing with percentages. Hence we can combine several
// processes running at the same time.
let currentProgress = 0;
let progressCount = 0;
// For each activity that is in progress, get its status.
this._activeProcesses.forEach(function (element) {
if (element.state ==
Components.interfaces.nsIActivityProcess.STATE_INPROGRESS &&
element.percentComplete != -1) {
currentProgress += element.percentComplete;
++progressCount;
}
});
// Add the generic progress that's fed to the status feedback object if
// we've got one.
if (this._statusFeedbackProgress != -1) {
currentProgress += this._statusFeedbackProgress;
++progressCount;
}
let percentage = 0;
if (progressCount) {
percentage = currentProgress / progressCount;
}
if (!percentage)
this._progressBar.setAttribute("mode", "undetermined");
else {
this._progressBar.setAttribute("mode", "determined");
this._progressBar.value = percentage;
this._progressBar.label = Math.round(percentage) + "%";
}
if (!this._progressBarVisible) {
this._progressBarContainer.removeAttribute('collapsed');
this._progressBarVisible = true;
}
}
else {
// Stop the bar spinning as we're not doing anything now.
this._progressBar.setAttribute("mode", "determined");
this._progressBar.value = 0;
this._progressBar.label = "";
if (this._progressBarVisible) {
this._progressBarContainer.collapsed = true;
this._progressBarVisible = false;
}
}
},
// nsIActivityMgrListener
onAddedActivity: function(aID, aActivity) {
// ignore Gloda activity for status bar purposes
if (aActivity.initiator == Gloda)
return;
if (aActivity instanceof Components.interfaces.nsIActivityEvent) {
this.showStatusString(aActivity.displayText);
}
else if (aActivity instanceof Components.interfaces.nsIActivityProcess) {
this._activeProcesses.push(aActivity);
aActivity.addListener(this);
this.startMeteors();
}
},
onRemovedActivity: function(aID) {
this._activeProcesses =
this._activeProcesses.filter(function (element) {
if (element.id == aID) {
element.removeListener(this);
this.stopMeteors();
return false;
}
return true;
}, this);
},
// nsIActivityListener
onStateChanged: function(aActivity, aOldState) {
},
onProgressChanged: function(aActivity, aStatusText, aWorkUnitsCompleted,
aTotalWorkUnits) {
let index = this._activeProcesses.indexOf(aActivity);
// Iterate through the list trying to find the first active process, but
// only go as far as our process.
for (var i = 0; i < index; ++i) {
if (this._activeProcesses[i].status ==
Components.interfaces.nsIActivityProcess.STATE_INPROGRESS)
break;
}
// If the found activity was the same as our activity, update the status
// text.
if (i == index)
// Use the display text if we haven't got any status text. I'm assuming
// that the status text will be generally what we want to see on the
// status bar.
this.showStatusString(aStatusText ? aStatusText : aActivity.displayText);
this.updateProgress();
},
onHandlerChanged: function(aActivity) {
}
}
function nsMsgWindowCommands()
{
}
nsMsgWindowCommands.prototype =
{
QueryInterface : function(iid)
{
if (iid.equals(Components.interfaces.nsIMsgWindowCommands) ||
iid.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_NOINTERFACE;
},
selectFolder: function(folderUri)
{
gFolderTreeView.selectFolder(MailUtils.getFolderForURI(folderUri));
},
selectMessage: function(messageUri)
{
let msgHdr = messenger.msgHdrFromURI(messageUri);
gFolderDisplay.selectMessage(msgHdr);
},
clearMsgPane: function()
{
// This call happens as part of a display decision made by the nsMsgDBView
// instance. Strictly speaking, we don't want this. I think davida's
// patch will change this, so we can figure it out after that lands if
// there are issues.
ClearMessagePane();
}
}
/**
* Loads the mail start page.
*/
function loadStartPage(aForce)
{
// If the preference isn't enabled, then don't load anything.
if (!aForce && !Services.prefs.getBoolPref("mailnews.start_page.enabled"))
return;
gMessageNotificationBar.clearMsgNotifications();
let startpage = Services.urlFormatter.formatURLPref("mailnews.start_page.url");
if (startpage)
{
try {
let uri = Services.uriFixup.createFixupURI(startpage, 0);
GetMessagePaneFrame().location.href = uri.spec;
}
catch (e) {
Components.utils.reportError(e);
}
}
else
{
GetMessagePaneFrame().location.href = "about:blank";
}
}
/**
* Returns the browser element of the current tab.
* The zoom manager, view source and possibly some other functions still rely
* on the getBrowser function.
*/
function getBrowser()
{
let tabmail = document.getElementById('tabmail');
return tabmail ? tabmail.getBrowserForSelectedTab() : getMessagePaneBrowser();
}
/**
* Returns the browser element of the message pane.
*/
function getMessagePaneBrowser() {
return document.getElementById("messagepane");
}
/**
* This function is global and expected by toolkit to get the notification box
* for the browser for use with items like password manager.
*/
function getNotificationBox(aWindow) {
var tabmail = document.getElementById("tabmail");
var tabInfo = tabmail.tabInfo;
for (var i = 0; i < tabInfo.length; ++i) {
var browserFunc = tabInfo[i].mode.getBrowser ||
tabInfo[i].mode.tabType.getBrowser;
if (browserFunc) {
var possBrowser = browserFunc.call(tabInfo[i].mode.tabType, tabInfo[i]);
if (possBrowser && possBrowser.contentWindow == aWindow && possBrowser.parentNode.tagName == "notificationbox")
return possBrowser.parentNode;
}
}
return null;
}
// Given the server, open the twisty and the set the selection
// on inbox of that server.
// prompt if offline.
function OpenInboxForServer(server)
{
gFolderTreeView.selectFolder(GetInboxFolder(server));
if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail()) {
if (server.type != "imap")
GetMessagesForInboxOnServer(server);
}
}
/** Update state of zoom type (text vs. full) menu item. */
function UpdateFullZoomMenu() {
let cmdItem = document.getElementById("cmd_fullZoomToggle");
cmdItem.setAttribute("checked", !ZoomManager.useFullZoom);
}
/**
* This class implements nsIBadCertListener2. Its job is to prevent "bad cert"
* security dialogs from being shown to the user. Currently it puts up the
* cert override dialog, though we'd like to give the user more detailed
* information in the future.
*/
function BadCertHandler() {
}
BadCertHandler.prototype = {
// Suppress any certificate errors
notifyCertProblem: function(socketInfo, status, targetSite) {
setTimeout(InformUserOfCertError, 0, socketInfo, status, targetSite);
return true;
},
// nsIInterfaceRequestor
getInterface: function(iid) {
return this.QueryInterface(iid);
},
// nsISupports
QueryInterface: function(iid) {
if (!iid.equals(Components.interfaces.nsIBadCertListener2) &&
!iid.equals(Components.interfaces.nsIInterfaceRequestor) &&
!iid.equals(Components.interfaces.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
function InformUserOfCertError(socketInfo, status, targetSite)
{
let params = {
exceptionAdded : false,
sslStatus : status,
prefetchCert: true,
location : targetSite
};
window.openDialog('chrome://pippki/content/exceptionDialog.xul',
'','chrome,centerscreen,modal', params);
}
function nsBrowserAccess() { }
nsBrowserAccess.prototype = {
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIBrowserDOMWindow]),
openURI: function (aURI, aOpener, aWhere, aFlags) {
const nsIBrowserDOMWindow = Components.interfaces.nsIBrowserDOMWindow;
let isExternal = !!(aFlags & nsIBrowserDOMWindow.OPEN_EXTERNAL);
if (isExternal && aURI && aURI.schemeIs("chrome")) {
Services.console.logStringMessage("use -chrome command-line option to load external chrome urls\n");
return null;
}
let newWindow = null;
let loadflags = isExternal ?
Components.interfaces.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
if (aWhere != nsIBrowserDOMWindow.OPEN_NEWTAB)
Services.console.logStringMessage("Opening a URI in something other than a new tab is not supported, opening in new tab instead");
let win, needToFocusWin;
// Try the current window. If we're in a popup, fall back on the most
// recent browser window.
if (!window.document.documentElement.getAttribute("chromehidden"))
win = window;
else {
win = getMostRecentMailWindow();
needToFocusWin = true;
}
if (!win)
throw("Couldn't get a suitable window for openURI");
let loadInBackground =
Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground");
let tabmail = win.document.getElementById("tabmail");
let clickHandler = null;
let browser = tabmail.getBrowserForDocument(content);
if (browser)
clickHandler = browser.clickHandler;
let newTab = tabmail.openTab("contentTab", {contentPage: "about:blank",
background: loadInBackground,
clickHandler: clickHandler});
newWindow = newTab.browser.docShell
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindow);
try {
if (aURI) {
let referrer = null;
if (aOpener) {
let location = aOpener.location;
referrer = Services.io.newURI(location, null, null);
}
newWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation)
.loadURI(aURI.spec, loadflags, referrer, null, null);
}
if (needToFocusWin || (!loadInBackground && isExternal))
newWindow.focus();
} catch(e) {
}
return newWindow;
},
isTabContentWindow: function (aWindow) {
return false;
}
};
function MailSetCharacterSet(aEvent) {
if (aEvent.target.hasAttribute("charset")) {
msgWindow.mailCharacterSet = aEvent.target.getAttribute("charset");
msgWindow.charsetOverride = true;
gMessageDisplay.keyForCharsetOverride =
("messageKey" in gMessageDisplay.displayedMessage ?
gMessageDisplay.displayedMessage.messageKey : null);
}
messenger.setDocumentCharset(msgWindow.mailCharacterSet);
}