suite/mailnews/components/compose/content/MsgComposeCommands.js (3,032 lines of code) (raw):

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* 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/. */ var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm"); ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm"); const {FolderUtils} = ChromeUtils.import("resource:///modules/FolderUtils.jsm"); const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js"); const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); /** * interfaces */ var nsIMsgCompDeliverMode = Ci.nsIMsgCompDeliverMode; var nsIMsgCompSendFormat = Ci.nsIMsgCompSendFormat; var nsIMsgCompConvertible = Ci.nsIMsgCompConvertible; var nsIMsgCompType = Ci.nsIMsgCompType; var nsIMsgCompFormat = Ci.nsIMsgCompFormat; var nsIAbPreferMailFormat = Ci.nsIAbPreferMailFormat; var mozISpellCheckingEngine = Ci.mozISpellCheckingEngine; /** * In order to distinguish clearly globals that are initialized once when js load (static globals) and those that need to be * initialize every time a compose window open (globals), I (ducarroz) have decided to prefix by s... the static one and * by g... the other one. Please try to continue and repect this rule in the future. Thanks. */ /** * static globals, need to be initialized only once */ var sComposeMsgsBundle; var sBrandBundle; var sRDF = null; var sNameProperty = null; var sDictCount = 0; /** * Global message window object. This is used by mail-offline.js and therefore * should not be renamed. We need to avoid doing this kind of cross file global * stuff in the future and instead pass this object as parameter when needed by * functions in the other js file. */ var msgWindow; var gMessenger; /** * Global variables, need to be re-initialized every time mostly because * we need to release them when the window closes. */ var gHideMenus; var gMsgCompose; var gOriginalMsgURI; var gWindowLocked; var gSendLocked; var gContentChanged; var gAutoSaving; var gCurrentIdentity; var defaultSaveOperation; var gSendOrSaveOperationInProgress; var gCloseWindowAfterSave; var gSavedSendNowKey; var gSendFormat; var gLogComposePerformance; var gMsgIdentityElement; var gMsgAddressingWidgetElement; var gMsgSubjectElement; var gMsgAttachmentElement; var gMsgHeadersToolbarElement; var gComposeType; var gFormatToolbarHidden = false; var gBodyFromArgs; // i18n globals var gCharsetConvertManager; var gLastWindowToHaveFocus; var gReceiptOptionChanged; var gDSNOptionChanged; var gAttachVCardOptionChanged; var gAutoSaveInterval; var gAutoSaveTimeout; var gAutoSaveKickedIn; var gEditingDraft; var kComposeAttachDirPrefName = "mail.compose.attach.dir"; function InitializeGlobalVariables() { gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); gMsgCompose = null; gOriginalMsgURI = null; gWindowLocked = false; gContentChanged = false; gCurrentIdentity = null; defaultSaveOperation = "draft"; gSendOrSaveOperationInProgress = false; gAutoSaving = false; gCloseWindowAfterSave = false; gSavedSendNowKey = null; gSendFormat = nsIMsgCompSendFormat.AskUser; gCharsetConvertManager = Cc['@mozilla.org/charset-converter-manager;1'].getService(Ci.nsICharsetConverterManager); gHideMenus = false; // We are storing the value of the bool logComposePerformance inorder to // avoid logging unnecessarily. gLogComposePerformance = MailServices.compose.logComposePerformance; gLastWindowToHaveFocus = null; gReceiptOptionChanged = false; gDSNOptionChanged = false; gAttachVCardOptionChanged = false; msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"] .createInstance(Ci.nsIMsgWindow); MailServices.mailSession.AddMsgWindow(msgWindow); } InitializeGlobalVariables(); function ReleaseGlobalVariables() { gCurrentIdentity = null; gCharsetConvertManager = null; gMsgCompose = null; gOriginalMsgURI = null; gMessenger = null; sComposeMsgsBundle = null; sBrandBundle = null; MailServices.mailSession.RemoveMsgWindow(msgWindow); msgWindow = null; } function disableEditableFields() { gMsgCompose.editor.flags |= Ci.nsIEditor.eEditorReadonlyMask; var disableElements = document.getElementsByAttribute("disableonsend", "true"); for (let i = 0; i < disableElements.length; i++) disableElements[i].setAttribute('disabled', 'true'); } function enableEditableFields() { gMsgCompose.editor.flags &= ~Ci.nsIEditor.eEditorReadonlyMask; var enableElements = document.getElementsByAttribute("disableonsend", "true"); for (let i = 0; i < enableElements.length; i++) enableElements[i].removeAttribute('disabled'); } /** * Small helper function to check whether the node passed in is a signature. * Note that a text node is not a DOM element, hence .localName can't be used. */ function isSignature(aNode) { return ["DIV","PRE"].includes(aNode.nodeName) && aNode.classList.contains("moz-signature"); } var stateListener = { NotifyComposeFieldsReady: function() { ComposeFieldsReady(); updateSendCommands(true); }, NotifyComposeBodyReady: function() { this.useParagraph = gMsgCompose.composeHTML && Services.prefs.getBoolPref("mail.compose.default_to_paragraph"); this.editor = GetCurrentEditor(); this.paragraphState = document.getElementById("cmd_paragraphState"); // Look at the compose types which require action (nsIMsgComposeParams.idl): switch (gComposeType) { case Ci.nsIMsgCompType.MailToUrl: gBodyFromArgs = true; case Ci.nsIMsgCompType.New: case Ci.nsIMsgCompType.NewsPost: case Ci.nsIMsgCompType.ForwardAsAttachment: this.NotifyComposeBodyReadyNew(); break; case Ci.nsIMsgCompType.Reply: case Ci.nsIMsgCompType.ReplyAll: case Ci.nsIMsgCompType.ReplyToSender: case Ci.nsIMsgCompType.ReplyToGroup: case Ci.nsIMsgCompType.ReplyToSenderAndGroup: case Ci.nsIMsgCompType.ReplyWithTemplate: case Ci.nsIMsgCompType.ReplyToList: this.NotifyComposeBodyReadyReply(); break; case Ci.nsIMsgCompType.ForwardInline: this.NotifyComposeBodyReadyForwardInline(); break; case Ci.nsIMsgCompType.EditTemplate: defaultSaveOperation = "template"; case Ci.nsIMsgCompType.Draft: case Ci.nsIMsgCompType.Template: case Ci.nsIMsgCompType.Redirect: case Ci.nsIMsgCompType.EditAsNew: break; default: dump("Unexpected nsIMsgCompType in NotifyComposeBodyReady (" + gComposeType + ")\n"); } // Set the selected item in the identity list as needed, which will cause // an identity/signature switch. This can only be done once the message // body has already been assembled with the signature we need to switch. if (gMsgCompose.identity != gCurrentIdentity) { // Since switching the signature loses the caret position, we record it // and restore it later. let selection = this.editor.selection; let range = selection.getRangeAt(0); let start = range.startOffset; let startNode = range.startContainer; this.editor.enableUndo(false); let identityList = GetMsgIdentityElement(); identityList.selectedItem = identityList.getElementsByAttribute( "identitykey", gMsgCompose.identity.key)[0]; LoadIdentity(false); this.editor.enableUndo(true); this.editor.resetModificationCount(); selection.collapse(startNode, start); } if (gMsgCompose.composeHTML) loadHTMLMsgPrefs(); AdjustFocus(); }, NotifyComposeBodyReadyNew: function() { let insertParagraph = this.useParagraph; let mailDoc = document.getElementById("content-frame").contentDocument; let mailBody = mailDoc.querySelector("body"); if (insertParagraph && gBodyFromArgs) { // Check for "empty" body before allowing paragraph to be inserted. // Non-empty bodies in a new message can occur when clicking on a // mailto link or when using the command line option -compose. // An "empty" body can be one of these two cases: // 1) <br> and nothing follows (no next sibling) // 2) <div/pre class="moz-signature"> // Note that <br><div/pre class="moz-signature"> doesn't happen in // paragraph mode. let firstChild = mailBody.firstChild; if ((firstChild.nodeName != "BR" || firstChild.nextSibling) && !isSignature(firstChild)) insertParagraph = false; } // Control insertion of line breaks. if (insertParagraph) { this.editor.enableUndo(false); this.editor.selection.collapse(mailBody, 0); let pElement = this.editor.createElementWithDefaults("p"); let brElement = this.editor.createElementWithDefaults("br"); pElement.appendChild(brElement); this.editor.insertElementAtSelection(pElement, false); this.paragraphState.setAttribute("state", "p"); this.editor.beginningOfDocument(); this.editor.enableUndo(true); this.editor.resetModificationCount(); } else { this.paragraphState.setAttribute("state", ""); } }, NotifyComposeBodyReadyReply: function() { // Control insertion of line breaks. if (this.useParagraph) { let mailDoc = document.getElementById("content-frame").contentDocument; let mailBody = mailDoc.querySelector("body"); let selection = this.editor.selection; // Make sure the selection isn't inside the signature. if (isSignature(mailBody.firstChild)) selection.collapse(mailBody, 0); let range = selection.getRangeAt(0); let start = range.startOffset; if (start != range.endOffset) { // The selection is not collapsed, most likely due to the // "select the quote" option. In this case we do nothing. return; } if (range.startContainer != mailBody) { dump("Unexpected selection in NotifyComposeBodyReadyReply\n"); return; } this.editor.enableUndo(false); let pElement = this.editor.createElementWithDefaults("p"); let brElement = this.editor.createElementWithDefaults("br"); pElement.appendChild(brElement); this.editor.insertElementAtSelection(pElement, false); // Position into the paragraph. selection.collapse(pElement, 0); this.paragraphState.setAttribute("state", "p"); this.editor.enableUndo(true); this.editor.resetModificationCount(); } else { this.paragraphState.setAttribute("state", ""); } }, NotifyComposeBodyReadyForwardInline: function() { let mailDoc = document.getElementById("content-frame").contentDocument; let mailBody = mailDoc.querySelector("body"); let selection = this.editor.selection; this.editor.enableUndo(false); // Control insertion of line breaks. selection.collapse(mailBody, 0); if (this.useParagraph) { let pElement = this.editor.createElementWithDefaults("p"); let brElement = this.editor.createElementWithDefaults("br"); pElement.appendChild(brElement); this.editor.insertElementAtSelection(pElement, false); this.paragraphState.setAttribute("state", "p"); } else { // insertLineBreak() has been observed to insert two <br> elements // instead of one before a <div>, so we'll do it ourselves here. let brElement = this.editor.createElementWithDefaults("br"); this.editor.insertElementAtSelection(brElement, false); this.paragraphState.setAttribute("state", ""); } this.editor.beginningOfDocument(); this.editor.enableUndo(true); this.editor.resetModificationCount(); }, ComposeProcessDone: function(aResult) { gWindowLocked = false; enableEditableFields(); updateComposeItems(); if (aResult== Cr.NS_OK) { if (!gAutoSaving) SetContentAndBodyAsUnmodified(); if (gCloseWindowAfterSave) { // Notify the SendListener that Send has been aborted and Stopped if (gMsgCompose) gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT); MsgComposeCloseWindow(); } } // else if we failed to save, and we're autosaving, need to re-mark the editor // as changed, so that we won't lose the changes. else if (gAutoSaving) { gMsgCompose.bodyModified = true; gContentChanged = true; } gAutoSaving = false; gCloseWindowAfterSave = false; }, SaveInFolderDone: function(folderURI) { DisplaySaveFolderDlg(folderURI); } }; // all progress notifications are done through the nsIWebProgressListener implementation... var progressListener = { onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { document.getElementById('navigator-throbber').setAttribute("busy", "true"); document.getElementById('compose-progressmeter').setAttribute( "mode", "undetermined" ); document.getElementById("statusbar-progresspanel").collapsed = false; } if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { gSendOrSaveOperationInProgress = false; document.getElementById('navigator-throbber').removeAttribute("busy"); document.getElementById('compose-progressmeter').setAttribute( "mode", "normal" ); document.getElementById('compose-progressmeter').setAttribute( "value", 0 ); document.getElementById("statusbar-progresspanel").collapsed = true; document.getElementById('statusText').setAttribute('label', ''); } }, onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { // Calculate percentage. var percent; if ( aMaxTotalProgress > 0 ) { percent = Math.round( (aCurTotalProgress*100)/aMaxTotalProgress ); if ( percent > 100 ) percent = 100; document.getElementById('compose-progressmeter').removeAttribute("mode"); // Advance progress meter. document.getElementById('compose-progressmeter').setAttribute( "value", percent ); } else { // Progress meter should be barber-pole in this case. document.getElementById('compose-progressmeter').setAttribute( "mode", "undetermined" ); } }, onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { // we can ignore this notification }, onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) { // Looks like it's possible that we get call while the document has been already delete! // therefore we need to protect ourself by using try/catch try { let statusText = document.getElementById("statusText"); if (statusText) statusText.setAttribute("label", aMessage); } catch (ex) {} }, onSecurityChange: function(aWebProgress, aRequest, state) { // we can ignore this notification }, QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), }; var defaultController = { supportsCommand: function(command) { switch (command) { //File Menu case "cmd_attachFile": case "cmd_attachPage": case "cmd_close": case "cmd_save": case "cmd_saveAsFile": case "cmd_saveAsDraft": case "cmd_saveAsTemplate": case "cmd_sendButton": case "cmd_sendNow": case "cmd_sendWithCheck": case "cmd_sendLater": case "cmd_printSetup": case "cmd_printpreview": case "cmd_print": //Edit Menu case "cmd_account": case "cmd_preferences": //Options Menu case "cmd_selectAddress": case "cmd_outputFormat": case "cmd_quoteMessage": return true; default: return false; } }, isCommandEnabled: function(command) { var composeHTML = gMsgCompose && gMsgCompose.composeHTML; switch (command) { //File Menu case "cmd_attachFile": case "cmd_attachPage": case "cmd_close": case "cmd_save": case "cmd_saveAsFile": case "cmd_saveAsDraft": case "cmd_saveAsTemplate": case "cmd_printSetup": case "cmd_printpreview": case "cmd_print": return !gWindowLocked; case "cmd_sendButton": case "cmd_sendLater": case "cmd_sendWithCheck": case "cmd_sendButton": return !gWindowLocked && !gSendLocked; case "cmd_sendNow": return !gWindowLocked && !Services.io.offline && !gSendLocked; //Edit Menu case "cmd_account": case "cmd_preferences": return true; //Options Menu case "cmd_selectAddress": return !gWindowLocked; case "cmd_outputFormat": return composeHTML; case "cmd_quoteMessage": var selectedURIs = GetSelectedMessages(); if (selectedURIs && selectedURIs.length > 0) return true; return false; default: return false; } }, doCommand: function(command) { switch (command) { //File Menu case "cmd_attachFile" : if (defaultController.isCommandEnabled(command)) AttachFile(); break; case "cmd_attachPage" : AttachPage(); break; case "cmd_close" : DoCommandClose(); break; case "cmd_save" : Save(); break; case "cmd_saveAsFile" : SaveAsFile(true); break; case "cmd_saveAsDraft" : SaveAsDraft(); break; case "cmd_saveAsTemplate" : SaveAsTemplate(); break; case "cmd_sendButton" : if (defaultController.isCommandEnabled(command)) { if (Services.io.offline) SendMessageLater(); else SendMessage(); } break; case "cmd_sendNow" : if (defaultController.isCommandEnabled(command)) SendMessage(); break; case "cmd_sendWithCheck" : if (defaultController.isCommandEnabled(command)) SendMessageWithCheck(); break; case "cmd_sendLater" : if (defaultController.isCommandEnabled(command)) SendMessageLater(); break; case "cmd_printSetup" : PrintUtils.showPageSetup(); break; case "cmd_printpreview" : PrintUtils.printPreview(PrintPreviewListener); break; case "cmd_print" : let browser = GetCurrentEditorElement(); PrintUtils.printWindow(browser.outerWindowID, browser); break; //Edit Menu case "cmd_account" : let currentAccountKey = getCurrentAccountKey(); let account = MailServices.accounts.getAccount(currentAccountKey); MsgAccountManager(null, account.incomingServer); break; case "cmd_preferences" : DoCommandPreferences(); break; //Options Menu case "cmd_selectAddress" : if (defaultController.isCommandEnabled(command)) SelectAddress(); break; case "cmd_quoteMessage" : if (defaultController.isCommandEnabled(command)) QuoteSelectedMessage(); break; default: return; } }, onEvent: function(event) { } }; var gAttachmentBucketController = { supportsCommand: function(aCommand) { switch (aCommand) { case "cmd_delete": case "cmd_renameAttachment": case "cmd_selectAll": case "cmd_openAttachment": return true; default: return false; } }, isCommandEnabled: function(aCommand) { switch (aCommand) { case "cmd_delete": return MessageGetNumSelectedAttachments() > 0; case "cmd_renameAttachment": return MessageGetNumSelectedAttachments() == 1; case "cmd_selectAll": return MessageHasAttachments(); case "cmd_openAttachment": return MessageGetNumSelectedAttachments() == 1; default: return false; } }, doCommand: function(aCommand) { switch (aCommand) { case "cmd_delete": if (MessageGetNumSelectedAttachments() > 0) RemoveSelectedAttachment(); break; case "cmd_renameAttachment": if (MessageGetNumSelectedAttachments() == 1) RenameSelectedAttachment(); break; case "cmd_selectAll": if (MessageHasAttachments()) SelectAllAttachments(); break; case "cmd_openAttachment": if (MessageGetNumSelectedAttachments() == 1) OpenSelectedAttachment(); break; default: return; } }, onEvent: function(event) { } }; function QuoteSelectedMessage() { var selectedURIs = GetSelectedMessages(); if (selectedURIs) for (let i = 0; i < selectedURIs.length; i++) gMsgCompose.quoteMessage(selectedURIs[i]); } function GetSelectedMessages() { var mailWindow = gMsgCompose && Services.wm.getMostRecentWindow("mail:3pane"); return mailWindow && mailWindow.gFolderDisplay.selectedMessageUris; } function SetupCommandUpdateHandlers() { top.controllers.appendController(defaultController); let attachmentBucket = document.getElementById("attachmentBucket"); attachmentBucket.controllers.appendController(gAttachmentBucketController); document.getElementById("optionsMenuPopup") .addEventListener("popupshowing", updateOptionItems, true); } function UnloadCommandUpdateHandlers() { document.getElementById("optionsMenuPopup") .removeEventListener("popupshowing", updateOptionItems, true); top.controllers.removeController(defaultController); let attachmentBucket = document.getElementById("attachmentBucket"); attachmentBucket.controllers.removeController(gAttachmentBucketController); } function CommandUpdate_MsgCompose() { var focusedWindow = top.document.commandDispatcher.focusedWindow; // we're just setting focus to where it was before if (focusedWindow == gLastWindowToHaveFocus) { return; } gLastWindowToHaveFocus = focusedWindow; updateComposeItems(); } function updateComposeItems() { try { // Edit Menu goUpdateCommand("cmd_rewrap"); // Insert Menu if (gMsgCompose && gMsgCompose.composeHTML) { goUpdateCommand("cmd_renderedHTMLEnabler"); goUpdateCommand("cmd_decreaseFontStep"); goUpdateCommand("cmd_increaseFontStep"); goUpdateCommand("cmd_bold"); goUpdateCommand("cmd_italic"); goUpdateCommand("cmd_underline"); goUpdateCommand("cmd_ul"); goUpdateCommand("cmd_ol"); goUpdateCommand("cmd_indent"); goUpdateCommand("cmd_outdent"); goUpdateCommand("cmd_align"); goUpdateCommand("cmd_smiley"); } // Options Menu goUpdateCommand("cmd_spelling"); } catch(e) {} } function openEditorContextMenu(popup) { gContextMenu = new nsContextMenu(popup); if (gContextMenu.shouldDisplay) { // If message body context menu then focused element should be content. var showPasteExtra = top.document.commandDispatcher.focusedWindow == content; gContextMenu.showItem("context-pasteNoFormatting", showPasteExtra); gContextMenu.showItem("context-pasteQuote", showPasteExtra); if (showPasteExtra) { goUpdateCommand("cmd_pasteNoFormatting"); goUpdateCommand("cmd_pasteQuote"); } return true; } return false; } function updateEditItems() { goUpdateCommand("cmd_pasteNoFormatting"); goUpdateCommand("cmd_pasteQuote"); goUpdateCommand("cmd_delete"); goUpdateCommand("cmd_renameAttachment"); goUpdateCommand("cmd_selectAll"); goUpdateCommand("cmd_openAttachment"); goUpdateCommand("cmd_findReplace"); goUpdateCommand("cmd_find"); goUpdateCommand("cmd_findNext"); goUpdateCommand("cmd_findPrev"); } function updateOptionItems() { goUpdateCommand("cmd_quoteMessage"); } /** * Update all the commands for sending a message to reflect their current state. */ function updateSendCommands(aHaveController) { updateSendLock(); if (aHaveController) { goUpdateCommand("cmd_sendButton"); goUpdateCommand("cmd_sendNow"); goUpdateCommand("cmd_sendLater"); goUpdateCommand("cmd_sendWithCheck"); } else { goSetCommandEnabled("cmd_sendButton", defaultController.isCommandEnabled("cmd_sendButton")); goSetCommandEnabled("cmd_sendNow", defaultController.isCommandEnabled("cmd_sendNow")); goSetCommandEnabled("cmd_sendLater", defaultController.isCommandEnabled("cmd_sendLater")); goSetCommandEnabled("cmd_sendWithCheck", defaultController.isCommandEnabled("cmd_sendWithCheck")); } } var messageComposeOfflineQuitObserver = { observe: function(aSubject, aTopic, aState) { // sanity checks if (aTopic == "network:offline-status-changed") { MessageComposeOfflineStateChanged(aState == "offline"); } // check whether to veto the quit request (unless another observer already // did) else if (aTopic == "quit-application-requested" && aSubject instanceof Ci.nsISupportsPRBool && !aSubject.data) aSubject.data = !ComposeCanClose(); } } function AddMessageComposeOfflineQuitObserver() { Services.obs.addObserver(messageComposeOfflineQuitObserver, "network:offline-status-changed"); Services.obs.addObserver(messageComposeOfflineQuitObserver, "quit-application-requested"); // set the initial state of the send button MessageComposeOfflineStateChanged(Services.io.offline); } function RemoveMessageComposeOfflineQuitObserver() { Services.obs.removeObserver(messageComposeOfflineQuitObserver, "network:offline-status-changed"); Services.obs.removeObserver(messageComposeOfflineQuitObserver, "quit-application-requested"); } function MessageComposeOfflineStateChanged(goingOffline) { try { var sendButton = document.getElementById("button-send"); var sendNowMenuItem = document.getElementById("menu_sendNow"); if (!gSavedSendNowKey) { gSavedSendNowKey = sendNowMenuItem.getAttribute('key'); } // don't use goUpdateCommand here ... the defaultController might not be installed yet updateSendCommands(false); if (goingOffline) { sendButton.label = sendButton.getAttribute('later_label'); sendButton.setAttribute('tooltiptext', sendButton.getAttribute('later_tooltiptext')); sendNowMenuItem.removeAttribute('key'); } else { sendButton.label = sendButton.getAttribute('now_label'); sendButton.setAttribute('tooltiptext', sendButton.getAttribute('now_tooltiptext')); if (gSavedSendNowKey) { sendNowMenuItem.setAttribute('key', gSavedSendNowKey); } } } catch(e) {} } function DoCommandClose() { if (ComposeCanClose()) { // Notify the SendListener that Send has been aborted and Stopped if (gMsgCompose) gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT); // note: if we're not caching this window, this destroys it for us MsgComposeCloseWindow(); } return false; } function DoCommandPreferences() { goPreferences('composing_messages_pane'); } function toggleAffectedChrome(aHide) { // chrome to toggle includes: // (*) menubar // (*) toolbox // (*) sidebar // (*) statusbar if (!gChromeState) gChromeState = {}; var statusbar = document.getElementById("status-bar"); // sidebar states map as follows: // hidden => hide/show nothing // collapsed => hide/show only the splitter // shown => hide/show the splitter and the box if (aHide) { // going into print preview mode gChromeState.sidebar = SidebarGetState(); SidebarSetState("hidden"); // deal with the Status Bar gChromeState.statusbarWasHidden = statusbar.hidden; statusbar.hidden = true; } else { // restoring normal mode (i.e., leaving print preview mode) SidebarSetState(gChromeState.sidebar); // restore the Status Bar statusbar.hidden = gChromeState.statusbarWasHidden; } // if we are unhiding and sidebar used to be there rebuild it if (!aHide && gChromeState.sidebar == "visible") SidebarRebuild(); getMailToolbox().hidden = aHide; document.getElementById("appcontent").collapsed = aHide; } var PrintPreviewListener = { getPrintPreviewBrowser() { var browser = document.getElementById("ppBrowser"); if (!browser) { browser = document.createElement("browser"); browser.setAttribute("id", "ppBrowser"); browser.setAttribute("flex", "1"); browser.setAttribute("disablehistory", "true"); browser.setAttribute("disablesecurity", "true"); browser.setAttribute("type", "content"); document.getElementById("sidebar-parent") .insertBefore(browser, document.getElementById("appcontent")); } return browser; }, getSourceBrowser() { return GetCurrentEditorElement(); }, getNavToolbox() { return getMailToolbox(); }, onEnter() { toggleAffectedChrome(true); }, onExit() { document.getElementById("ppBrowser").collapsed = true; toggleAffectedChrome(false); } } function ToggleWindowLock() { gWindowLocked = !gWindowLocked; updateComposeItems(); } /* This function will go away soon as now arguments are passed to the window using a object of type nsMsgComposeParams instead of a string */ function GetArgs(originalData) { var args = new Object(); if (originalData == "") return null; var data = ""; var separator = String.fromCharCode(1); var quoteChar = ""; var prevChar = ""; var nextChar = ""; for (let i = 0; i < originalData.length; i++, prevChar = aChar) { var aChar = originalData.charAt(i) var aCharCode = originalData.charCodeAt(i) if ( i < originalData.length - 1) nextChar = originalData.charAt(i + 1); else nextChar = ""; if (aChar == quoteChar && (nextChar == "," || nextChar == "")) { quoteChar = ""; data += aChar; } else if ((aCharCode == 39 || aCharCode == 34) && prevChar == "=") //quote or double quote { if (quoteChar == "") quoteChar = aChar; data += aChar; } else if (aChar == ",") { if (quoteChar == "") data += separator; else data += aChar } else data += aChar } var pairs = data.split(separator); for (let i = pairs.length - 1; i >= 0; i--) { var pos = pairs[i].indexOf('='); if (pos == -1) continue; var argname = pairs[i].substring(0, pos); var argvalue = pairs[i].substring(pos + 1); if (argvalue.charAt(0) == "'" && argvalue.charAt(argvalue.length - 1) == "'") args[argname] = argvalue.substring(1, argvalue.length - 1); else try { args[argname] = decodeURIComponent(argvalue); } catch (e) {args[argname] = argvalue;} // dump("[" + argname + "=" + args[argname] + "]\n"); } return args; } function ComposeFieldsReady() { //If we are in plain text, we need to set the wrap column if (! gMsgCompose.composeHTML) { try { gMsgCompose.editor.QueryInterface(Ci.nsIEditorMailSupport).wrapWidth = gMsgCompose.wrapLength; } catch (e) { dump("### textEditor.wrapWidth exception text: " + e + " - failed\n"); } } CompFields2Recipients(gMsgCompose.compFields); SetComposeWindowTitle(); enableEditableFields(); } // checks if the passed in string is a mailto url, if it is, generates nsIMsgComposeParams // for the url and returns them. function handleMailtoArgs(mailtoUrl) { // see if the string is a mailto url....do this by checking the first 7 characters of the string if (/^mailto:/i.test(mailtoUrl)) { // if it is a mailto url, turn the mailto url into a MsgComposeParams object.... var uri = Services.io.newURI(mailtoUrl); if (uri) return MailServices.compose.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", Ci.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(Ci.nsIFileProtocolHandler) .getFileFromURLSpec(img.src); } catch (ex) { continue; } if (!nsFile.exists()) { continue; } if (!tmpD.contains(nsFile)) { // Not anywhere under the temp dir. continue; } let contentType = Cc["@mozilla.org/mime;1"] .getService(Ci.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 = Cc["@mozilla.org/parserutils;1"] .getService(Ci.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 gBodyFromArgs = false; if (aParams) params = aParams; else if (window.arguments && window.arguments[0]) { try { if (window.arguments[0] instanceof Ci.nsIMsgComposeParams) params = window.arguments[0]; else params = handleMailtoArgs(window.arguments[0]); } catch(ex) { dump("ERROR with parameters: " + ex + "\n"); } // if still no dice, try and see if the params is an old fashioned list of string attributes // XXX can we get rid of this yet? if (!params) { args = GetArgs(window.arguments[0]); } } // Set the document language to the preference as early as possible. document.documentElement .setAttribute("lang", Services.prefs.getCharPref("spellchecker.dictionary")); var identityList = GetMsgIdentityElement(); document.addEventListener("paste", onPasteOrDrop); document.addEventListener("drop", onPasteOrDrop); 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 = Cc["@mozilla.org/messengercompose/composeparams;1"] .createInstance(Ci.nsIMsgComposeParams); params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"] .createInstance(Ci.nsIMsgCompFields); if (args) { //Convert old fashion arguments into params var composeFields = params.composeFields; if (args.bodyislink && args.bodyislink == "true") params.bodyIsLink = true; if (args.type) params.type = args.type; if (args.format) { // Only use valid values. if (args.format == Ci.nsIMsgCompFormat.PlainText || args.format == Ci.nsIMsgCompFormat.HTML || args.format == Ci.nsIMsgCompFormat.OppositeOfDefault) params.format = args.format; else if (args.format.toLowerCase().trim() == "html") params.format = Ci.nsIMsgCompFormat.HTML; else if (args.format.toLowerCase().trim() == "text") params.format = Ci.nsIMsgCompFormat.PlainText; } if (args.originalMsgURI) params.originalMsgURI = args.originalMsgURI; if (args.preselectid) params.identity = getIdentityForKey(args.preselectid); if (args.from) composeFields.from = args.from; if (args.to) composeFields.to = args.to; if (args.cc) composeFields.cc = args.cc; if (args.bcc) composeFields.bcc = args.bcc; if (args.newsgroups) composeFields.newsgroups = args.newsgroups; if (args.subject) composeFields.subject = args.subject; if (args.attachment) { var attachmentList = args.attachment.split(","); var commandLine = Cc["@mozilla.org/toolkit/command-line;1"] .createInstance(); for (let i = 0; i < attachmentList.length; i++) { let attachmentStr = attachmentList[i]; let uri = commandLine.resolveURI(attachmentStr); let attachment = Cc["@mozilla.org/messengercompose/attachment;1"] .createInstance(Ci.nsIMsgAttachment); if (uri instanceof Ci.nsIFileURL) { if (uri.file.exists()) attachment.size = uri.file.fileSize; else attachment = null; } // Only want to attach if a file that exists or it is not a file. if (attachment) { attachment.url = uri.spec; composeFields.addAttachment(attachment); } else { let title = sComposeMsgsBundle.getString("errorFileAttachTitle"); let msg = sComposeMsgsBundle.getFormattedString("errorFileAttachMessage", [attachmentStr]); Services.prompt.alert(null, title, msg); } } } if (args.newshost) composeFields.newshost = args.newshost; if (args.message) { let msgFile = Cc["@mozilla.org/file/local;1"] .createInstance(Ci.nsIFile); if (OS.Path.dirname(args.message) == ".") { let workingDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); args.message = OS.Path.join(workingDir.path, OS.Path.basename(args.message)); } msgFile.initWithPath(args.message); if (!msgFile.exists()) { let title = sComposeMsgsBundle.getString("errorFileMessageTitle"); let msg = sComposeMsgsBundle.getFormattedString("errorFileMessageMessage", [args.message]); Services.prompt.alert(null, title, msg); } else { let data = ""; let fstream = null; let cstream = null; try { fstream = Cc["@mozilla.org/network/file-input-stream;1"] .createInstance(Ci.nsIFileInputStream); cstream = Cc["@mozilla.org/intl/converter-input-stream;1"] .createInstance(Ci.nsIConverterInputStream); fstream.init(msgFile, -1, 0, 0); // Open file in default/read-only mode. cstream.init(fstream, "UTF-8", 0, 0); let str = {}; let read = 0; do { // Read as much as we can and put it in str.value. read = cstream.readString(0xffffffff, str); data += str.value; } while (read != 0); } catch (e) { let title = sComposeMsgsBundle.getString("errorFileMessageTitle"); let msg = sComposeMsgsBundle.getFormattedString("errorLoadFileMessageMessage", [args.message]); Services.prompt.alert(null, title, msg); } finally { if (cstream) cstream.close(); if (fstream) fstream.close(); } if (data) { let pos = data.search(/\S/); // Find first non-whitespace character. if (params.format != Ci.nsIMsgCompFormat.PlainText && (args.message.endsWith(".htm") || args.message.endsWith(".html") || data.substr(pos, 14).toLowerCase() == "<!doctype html" || data.substr(pos, 5).toLowerCase() == "<html")) { // We replace line breaks because otherwise they'll be converted // to <br> in nsMsgCompose::BuildBodyMessageAndSignature(). // Don't do the conversion if the user asked explicitly for plain // text. data = data.replace(/\r?\n/g, " "); } gBodyFromArgs = true; composeFields.body = data; } } } else if (args.body) { gBodyFromArgs = true; composeFields.body = args.body; } } } gComposeType = params.type; // Detect correct identity when missing or mismatched. // An identity with no email is likely not valid. // When editing a draft, 'params.identity' is pre-populated with the identity // that created the draft or the identity owning the draft folder for a // "foreign", draft, see ComposeMessage() in mailCommands.js. We don't want // the latter, so use the creator identity which could be null. if (gComposeType == Ci.nsIMsgCompType.Draft) { let creatorKey = params.composeFields.creatorIdentityKey; params.identity = creatorKey ? getIdentityForKey(creatorKey) : null; } let from = []; if (params.composeFields.from) from = MailServices.headerParser .parseEncodedHeader(params.composeFields.from, null); from = (from.length && from[0] && from[0].email) ? from[0].email.toLowerCase().trim() : null; if (!params.identity || !params.identity.email || (from && !emailSimilar(from, params.identity.email))) { let identities = MailServices.accounts.allIdentities; let suitableCount = 0; // Search for a matching identity. if (from) { for (let ident of identities) { if (ident.email && from == ident.email.toLowerCase()) { if (suitableCount == 0) params.identity = ident; suitableCount++; if (suitableCount > 1) break; // No need to find more, it's already not unique. } } } if (!params.identity || !params.identity.email) { let identity = null; // No preset identity and no match, so use the default account. let defaultAccount = MailServices.accounts.defaultAccount; if (defaultAccount) { identity = defaultAccount.defaultIdentity; } if (!identity) { // Get the first identity we have in the list. let identitykey = identityList.getItemAtIndex(0).getAttribute("identitykey"); identity = MailServices.accounts.getIdentity(identitykey); } params.identity = identity; } // Warn if no or more than one match was found. // But don't warn for +suffix additions (a+b@c.com). if (from && (suitableCount > 1 || (suitableCount == 0 && !emailSimilar(from, params.identity.email)))) gComposeNotificationBar.setIdentityWarning(params.identity.identityName); } identityList.selectedItem = identityList.getElementsByAttribute("identitykey", params.identity.key)[0]; if (params.composeFields.from) identityList.value = MailServices.headerParser.parseDecodedHeader(params.composeFields.from)[0].toString(); LoadIdentity(true); // 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 = MailServices.compose.initCompose(params, window, editorElement.docShell); document.getElementById("returnReceiptMenu") .setAttribute("checked", gMsgCompose.compFields.returnReceipt); document.getElementById("dsnMenu") .setAttribute('checked', gMsgCompose.compFields.DSN); document.getElementById("cmd_attachVCard") .setAttribute("checked", gMsgCompose.compFields.attachVCard); document.getElementById("menu_inlineSpellCheck") .setAttribute("checked", Services.prefs.getBoolPref("mail.spellcheck.inline")); let editortype = gMsgCompose.composeHTML ? "htmlmail" : "textmail"; editorElement.makeEditable(editortype, true); // setEditorType MUST be call before setContentWindow if (gMsgCompose.composeHTML) { initLocalFontFaceMenu(document.getElementById("FontFacePopup")); } else { //Remove HTML toolbar, format and insert menus as we are editing in plain //text mode. let toolbar = document.getElementById("FormatToolbar"); toolbar.hidden = true; toolbar.setAttribute("hideinmenu", "true"); document.getElementById("outputFormatMenu").setAttribute("hidden", true); document.getElementById("formatMenu").setAttribute("hidden", true); document.getElementById("insertMenu").setAttribute("hidden", true); } // Do setup common to Message Composer and Web Composer. EditorSharedStartup(); if (params.bodyIsLink) { let body = gMsgCompose.compFields.body; if (gMsgCompose.composeHTML) { let cleanBody; try { cleanBody = decodeURI(body); } catch(e) { cleanBody = body; } body = body.replace(/&/g, "&amp;"); gMsgCompose.compFields.body = "<br /><a href=\"" + body + "\">" + cleanBody + "</a><br />"; } else { gMsgCompose.compFields.body = "\n<" + body + ">\n"; } } GetMsgSubjectElement().value = gMsgCompose.compFields.subject; var attachments = gMsgCompose.compFields.attachments; while (attachments.hasMoreElements()) { AddAttachment(attachments.getNext().QueryInterface(Ci.nsIMsgAttachment)); } var event = document.createEvent('Events'); event.initEvent('compose-window-init', false, true); document.getElementById("msgcomposeWindow").dispatchEvent(event); gMsgCompose.RegisterStateListener(stateListener); // Add an observer to be called when document is done loading, // which creates the editor. try { GetCurrentCommandManager().addCommandObserver(gMsgEditorCreationObserver, "obs_documentCreated"); // Load empty page to create the editor editorElement.webNavigation.loadURI("about:blank", Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, // referrer null, // post-data stream null, // HTTP headers Services.scriptSecurityManager.getSystemPrincipal()); } catch (e) { dump(" Failed to startup editor: "+e+"\n"); } // create URI of the folder from draftId var draftId = gMsgCompose.compFields.draftId; var folderURI = draftId.substring(0, draftId.indexOf("#")).replace("-message", ""); try { var folder = sRDF.GetResource(folderURI); gEditingDraft = (folder instanceof Ci.nsIMsgFolder) && (folder.flags & Ci.nsMsgFolderFlags.Drafts); } catch (ex) { gEditingDraft = false; } gAutoSaveKickedIn = false; gAutoSaveInterval = Services.prefs.getBoolPref("mail.compose.autosave") ? Services.prefs.getIntPref("mail.compose.autosaveinterval") * 60000 : 0; if (gAutoSaveInterval) gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval); } function splitEmailAddress(aEmail) { let at = aEmail.lastIndexOf("@"); return (at != -1) ? [aEmail.slice(0, at), aEmail.slice(at + 1)] : [aEmail, ""]; } // Emails are equal ignoring +suffixes (email+suffix@example.com). function emailSimilar(a, b) { if (!a || !b) return a == b; a = splitEmailAddress(a.toLowerCase()); b = splitEmailAddress(b.toLowerCase()); return a[1] == b[1] && a[0].split("+", 1)[0] == b[0].split("+", 1)[0]; } // The new, nice, simple way of getting notified when a new editor has been created var gMsgEditorCreationObserver = { observe: function(aSubject, aTopic, aData) { if (aTopic == "obs_documentCreated") { var editor = GetCurrentEditor(); var commandManager = GetCurrentCommandManager(); if (editor && commandManager == aSubject) { let editorStyle = editor.QueryInterface(Ci.nsIEditorStyleSheets); // We use addOverrideStyleSheet rather than addStyleSheet so that we get // a synchronous load, rather than having a late-finishing async load // mark our editor as modified when the user hasn't typed anything yet, // but that means the sheet must not @import slow things, especially // not over the network. editorStyle.addOverrideStyleSheet("chrome://messenger/skin/messageQuotes.css"); InitEditor(editor); } // Now that we know this document is an editor, update commands now if // the document has focus, or next time it receives focus via // CommandUpdate_MsgCompose() if (gLastWindowToHaveFocus == document.commandDispatcher.focusedWindow) updateComposeItems(); else gLastWindowToHaveFocus = null; } } } function WizCallback(state) { if (state){ ComposeStartup(null); } else { // The account wizard is still closing so we can't close just yet setTimeout(MsgComposeCloseWindow, 0); } } function ComposeLoad() { sComposeMsgsBundle = document.getElementById("bundle_composeMsgs"); sBrandBundle = document.getElementById("brandBundle"); var otherHeaders = Services.prefs.getCharPref("mail.compose.other.header"); sRDF = Cc['@mozilla.org/rdf/rdf-service;1'] .getService(Ci.nsIRDFService); sNameProperty = sRDF.GetResource("http://home.netscape.com/NC-rdf#Name?sort=true"); AddMessageComposeOfflineQuitObserver(); if (gLogComposePerformance) MailServices.compose.TimeStamp("Start initializing the compose window (ComposeLoad)", false); msgWindow.notificationCallbacks = new nsMsgBadCertHandler(); try { SetupCommandUpdateHandlers(); // This will do migration, or create a new account if we need to. // We also want to open the account wizard if no identities are found var state = verifyAccounts(WizCallback, true); if (otherHeaders) { var selectNode = document.getElementById('addressCol1#1'); var otherHeaders_Array = otherHeaders.split(","); for (let i = 0; i < otherHeaders_Array.length; i++) selectNode.appendItem(otherHeaders_Array[i] + ":", "addr_other"); } if (state) ComposeStartup(null); } catch (ex) { Cu.reportError(ex); var errorTitle = sComposeMsgsBundle.getString("initErrorDlogTitle"); var errorMsg = sComposeMsgsBundle.getString("initErrorDlgMessage"); Services.prompt.alert(window, errorTitle, errorMsg); MsgComposeCloseWindow(); return; } if (gLogComposePerformance) MailServices.compose.TimeStamp("Done with the initialization (ComposeLoad). Waiting on editor to load about:blank", false); // Before and after callbacks for the customizeToolbar code var mailToolbox = getMailToolbox(); mailToolbox.customizeInit = MailToolboxCustomizeInit; mailToolbox.customizeDone = MailToolboxCustomizeDone; mailToolbox.customizeChange = MailToolboxCustomizeChange; } function ComposeUnload() { // Send notification that the window is going away completely. document.getElementById("msgcomposeWindow").dispatchEvent( new Event("compose-window-unload", { bubbles: false, cancelable: false })); GetCurrentCommandManager().removeCommandObserver(gMsgEditorCreationObserver, "obs_documentCreated"); UnloadCommandUpdateHandlers(); // Stop InlineSpellCheckerUI so personal dictionary is saved EnableInlineSpellCheck(false); EditorCleanup(); RemoveMessageComposeOfflineQuitObserver(); if (gMsgCompose) gMsgCompose.UnregisterStateListener(stateListener); if (gAutoSaveTimeout) clearTimeout(gAutoSaveTimeout); if (msgWindow) { msgWindow.closeWindow(); msgWindow.notificationCallbacks = null; } ReleaseGlobalVariables(); } function ComposeSetCharacterSet(aEvent) { if (gMsgCompose) SetDocumentCharacterSet(aEvent.target.getAttribute("charset")); else dump("Compose has not been created!\n"); } function SetDocumentCharacterSet(aCharset) { // Replace generic Japanese with ISO-2022-JP. if (aCharset == "Japanese") { aCharset = "ISO-2022-JP"; } gMsgCompose.SetDocumentCharset(aCharset); SetComposeWindowTitle(); } function GetCharsetUIString() { // The charset here is already the canonical charset (not an alias). let charset = gMsgCompose.compFields.characterSet; if (!charset) return ""; if (charset.toLowerCase() != gMsgCompose.compFields.defaultCharacterSet.toLowerCase()) { try { return " - " + gCharsetConvertManager.getCharsetTitle(charset); } catch(e) { // Not a canonical charset after all... Cu.reportError("Not charset title for charset=" + charset); return " - " + charset; } } return ""; } // Add-ons can override this to customize the behavior. function DoSpellCheckBeforeSend() { return Services.prefs.getBoolPref("mail.SpellCheckBeforeSend"); } /** * Handles message sending operations. * @param msgType nsIMsgCompDeliverMode of the operation. */ function GenericSendMessage(msgType) { var msgCompFields = gMsgCompose.compFields; Recipients2CompFields(msgCompFields); var address = GetMsgIdentityElement().value; address = MailServices.headerParser.makeFromDisplayAddress(address); msgCompFields.from = MailServices.headerParser.makeMimeHeader([address[0]]); var subject = GetMsgSubjectElement().value; msgCompFields.subject = subject; Attachments2CompFields(msgCompFields); if (msgType == Ci.nsIMsgCompDeliverMode.Now || msgType == Ci.nsIMsgCompDeliverMode.Later || msgType == Ci.nsIMsgCompDeliverMode.Background) { //Do we need to check the spelling? if (DoSpellCheckBeforeSend()) { // We disable spellcheck for the following -subject line, attachment // pane, identity and addressing widget therefore we need to explicitly // focus on the mail body when we have to do a spellcheck. SetMsgBodyFrameFocus(); window.cancelSendMessage = false; window.openDialog("chrome://editor/content/EdSpellCheck.xul", "_blank", "dialog,close,titlebar,modal,resizable", true, true, false); if (window.cancelSendMessage) return; } // Strip trailing spaces and long consecutive WSP sequences from the // subject line to prevent getting only WSP chars on a folded line. var fixedSubject = subject.replace(/\s{74,}/g, " ") .replace(/\s*$/, ""); if (fixedSubject != subject) { subject = fixedSubject; msgCompFields.subject = fixedSubject; GetMsgSubjectElement().value = fixedSubject; } // Remind the person if there isn't a subject. if (subject == "") { if (Services.prompt.confirmEx( window, sComposeMsgsBundle.getString("subjectEmptyTitle"), sComposeMsgsBundle.getString("subjectEmptyMessage"), (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1), sComposeMsgsBundle.getString("sendWithEmptySubjectButton"), sComposeMsgsBundle.getString("cancelSendingButton"), null, null, {value:0}) == 1) { GetMsgSubjectElement().focus(); return; } } // Check if the user tries to send a message to a newsgroup through a mail // account. var currentAccountKey = getCurrentAccountKey(); var account = MailServices.accounts.getAccount(currentAccountKey); if (!account) { throw "UNEXPECTED: currentAccountKey '" + currentAccountKey + "' has no matching account!"; } if (account.incomingServer.type != "nntp" && msgCompFields.newsgroups != "") { const kDontAskAgainPref = "mail.compose.dontWarnMail2Newsgroup"; // Default to ask user if the pref is not set. var dontAskAgain = Services.prefs.getBoolPref(kDontAskAgainPref); if (!dontAskAgain) { var checkbox = {value:false}; var okToProceed = Services.prompt.confirmCheck( window, sComposeMsgsBundle.getString("noNewsgroupSupportTitle"), sComposeMsgsBundle.getString("recipientDlogMessage"), sComposeMsgsBundle.getString("CheckMsg"), checkbox); if (!okToProceed) return; } if (checkbox.value) Services.prefs.setBoolPref(kDontAskAgainPref, true); // Remove newsgroups to prevent news_p to be set // in nsMsgComposeAndSend::DeliverMessage() msgCompFields.newsgroups = ""; } // Before sending the message, check what to do with HTML message, // eventually abort. var convert = DetermineConvertibility(); var action = DetermineHTMLAction(convert); // Check if e-mail addresses are complete, in case user has turned off // autocomplete to local domain. if (!CheckValidEmailAddress(msgCompFields.to, msgCompFields.cc, msgCompFields.bcc)) return; if (action == Ci.nsIMsgCompSendFormat.AskUser) { var recommAction = (convert == Ci.nsIMsgCompConvertible.No) ? Ci.nsIMsgCompSendFormat.AskUser : Ci.nsIMsgCompSendFormat.PlainText; var result2 = {action:recommAction, convertible:convert, abort:false}; window.openDialog("chrome://messenger/content/messengercompose/askSendFormat.xul", "askSendFormatDialog", "chrome,modal,titlebar,centerscreen", result2); if (result2.abort) return; action = result2.action; } // We will remember the users "send format" decision in the address // collector code (see nsAbAddressCollector::CollectAddress()) // by using msgCompFields.forcePlainText and // msgCompFields.useMultipartAlternative to determine the // nsIAbPreferMailFormat (unknown, plaintext, or html). // If the user sends both, we remember html. switch (action) { case Ci.nsIMsgCompSendFormat.PlainText: msgCompFields.forcePlainText = true; msgCompFields.useMultipartAlternative = false; break; case Ci.nsIMsgCompSendFormat.HTML: msgCompFields.forcePlainText = false; msgCompFields.useMultipartAlternative = false; break; case Ci.nsIMsgCompSendFormat.Both: msgCompFields.forcePlainText = false; msgCompFields.useMultipartAlternative = true; break; default: throw new Error("Invalid nsIMsgCompSendFormat action; action=" + action); } } // Hook for extra compose pre-processing. Services.obs.notifyObservers(window, "mail:composeOnSend"); var originalCharset = gMsgCompose.compFields.characterSet; // Check if the headers of composing mail can be converted to a mail charset. if (msgType == Ci.nsIMsgCompDeliverMode.Now || msgType == Ci.nsIMsgCompDeliverMode.Later || msgType == Ci.nsIMsgCompDeliverMode.Background || msgType == Ci.nsIMsgCompDeliverMode.Save || msgType == Ci.nsIMsgCompDeliverMode.SaveAsDraft || msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft || msgType == Ci.nsIMsgCompDeliverMode.SaveAsTemplate) { var fallbackCharset = new Object; // Check encoding, switch to UTF-8 if the default encoding doesn't fit // and disable_fallback_to_utf8 isn't set for this encoding. if (!gMsgCompose.checkCharsetConversion(getCurrentIdentity(), fallbackCharset)) { let disableFallback = Services.prefs .getBoolPref("mailnews.disable_fallback_to_utf8." + originalCharset, false); if (disableFallback) msgCompFields.needToCheckCharset = false; else fallbackCharset.value = "UTF-8"; } if (fallbackCharset && fallbackCharset.value && fallbackCharset.value != "") gMsgCompose.SetDocumentCharset(fallbackCharset.value); } try { // Just before we try to send the message, fire off the // compose-send-message event for listeners such as smime so they can do // any pre-security work such as fetching certificates before sending. var event = document.createEvent('UIEvents'); event.initEvent('compose-send-message', false, true); var msgcomposeWindow = document.getElementById("msgcomposeWindow"); msgcomposeWindow.setAttribute("msgtype", msgType); msgcomposeWindow.dispatchEvent(event); if (event.defaultPrevented) throw Cr.NS_ERROR_ABORT; gAutoSaving = (msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft); if (!gAutoSaving) { // Disable the ui if we're not auto-saving. gWindowLocked = true; disableEditableFields(); updateComposeItems(); } else { // If we're auto saving, mark the body as not changed here, and not // when the save is done, because the user might change it between now // and when the save is done. SetContentAndBodyAsUnmodified(); } var progress = Cc["@mozilla.org/messenger/progress;1"] .createInstance(Ci.nsIMsgProgress); if (progress) { progress.registerListener(progressListener); gSendOrSaveOperationInProgress = true; } msgWindow.domWindow = window; msgWindow.rootDocShell.allowAuth = true; gMsgCompose.SendMsg(msgType, getCurrentIdentity(), getCurrentAccountKey(), msgWindow, progress); } catch (ex) { Cu.reportError("GenericSendMessage FAILED: " + ex); gWindowLocked = false; enableEditableFields(); updateComposeItems(); } if (gMsgCompose && originalCharset != gMsgCompose.compFields.characterSet) SetDocumentCharacterSet(gMsgCompose.compFields.characterSet); } /** * Check if the given address is valid (contains a @). * * @param aAddress The address string to check. */ function isValidAddress(aAddress) { return (aAddress.includes("@", 1) && !aAddress.endsWith("@")); } /** * Keep the Send buttons disabled until any recipient is entered. */ function updateSendLock() { gSendLocked = true; if (!gMsgCompose) return; // Helper function to check for a valid list name. function isValidListName(aInput) { let listNames = MimeParser.parseHeaderField(aInput, MimeParser.HEADER_ADDRESS); return listNames.length > 0 && MailServices.ab.mailListNameExists(listNames[0].name); } const mailTypes = [ "addr_to", "addr_cc", "addr_bcc" ]; // Enable the send buttons if anything usable was entered into at least one // recipient field. for (let row = 1; row <= top.MAX_RECIPIENTS; row ++) { let popupValue = awGetPopupElement(row).value; let inputValue = awGetInputElement(row).value.trim(); // Check for a valid looking email address or a valid mailing list name // from one of our addressbooks. if ((mailTypes.includes(popupValue) && (isValidAddress(inputValue) || isValidListName(inputValue))) || ((popupValue == "addr_newsgroups") && (inputValue != ""))) { gSendLocked = false; break; } } } function CheckValidEmailAddress(aTo, aCC, aBCC) { var invalidStr = null; // crude check that the to, cc, and bcc fields contain at least one '@'. // We could parse each address, but that might be overkill. if (aTo.length > 0 && (aTo.indexOf("@") <= 0 && aTo.toLowerCase() != "postmaster" || aTo.indexOf("@") == aTo.length - 1)) invalidStr = aTo; else if (aCC.length > 0 && (aCC.indexOf("@") <= 0 && aCC.toLowerCase() != "postmaster" || aCC.indexOf("@") == aCC.length - 1)) invalidStr = aCC; else if (aBCC.length > 0 && (aBCC.indexOf("@") <= 0 && aBCC.toLowerCase() != "postmaster" || aBCC.indexOf("@") == aBCC.length - 1)) invalidStr = aBCC; if (invalidStr) { var errorTitle = sComposeMsgsBundle.getString("addressInvalidTitle"); var errorMsg = sComposeMsgsBundle.getFormattedString("addressInvalid", [invalidStr], 1); Services.prompt.alert(window, errorTitle, errorMsg); return false; } return true; } function SendMessage() { let sendInBackground = Services.prefs.getBoolPref("mailnews.sendInBackground"); if (sendInBackground && AppConstants.platform != "macosx") { let enumerator = Services.wm.getEnumerator(null); let count = 0; while (enumerator.hasMoreElements() && count < 2) { enumerator.getNext(); count++; } if (count == 1) sendInBackground = false; } GenericSendMessage(sendInBackground ? nsIMsgCompDeliverMode.Background : nsIMsgCompDeliverMode.Now); } function SendMessageWithCheck() { var warn = Services.prefs.getBoolPref("mail.warn_on_send_accel_key"); if (warn) { var checkValue = {value:false}; var buttonPressed = Services.prompt.confirmEx(window, sComposeMsgsBundle.getString('sendMessageCheckWindowTitle'), sComposeMsgsBundle.getString('sendMessageCheckLabel'), (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), sComposeMsgsBundle.getString('sendMessageCheckSendButtonLabel'), null, null, sComposeMsgsBundle.getString('CheckMsg'), checkValue); if (buttonPressed != 0) { return; } if (checkValue.value) { Services.prefs.setBoolPref("mail.warn_on_send_accel_key", false); } } if (Services.io.offline) SendMessageLater(); else SendMessage(); } function SendMessageLater() { GenericSendMessage(nsIMsgCompDeliverMode.Later); } function Save() { switch (defaultSaveOperation) { case "file" : SaveAsFile(false); break; case "template" : SaveAsTemplate(false); break; default : SaveAsDraft(false); break; } } function SaveAsFile(saveAs) { var subject = GetMsgSubjectElement().value; GetCurrentEditorElement().contentDocument.title = subject; if (gMsgCompose.bodyConvertible() == nsIMsgCompConvertible.Plain) SaveDocument(saveAs, false, "text/plain"); else SaveDocument(saveAs, false, "text/html"); defaultSaveOperation = "file"; } function SaveAsDraft() { GenericSendMessage(nsIMsgCompDeliverMode.SaveAsDraft); defaultSaveOperation = "draft"; gAutoSaveKickedIn = false; gEditingDraft = true; } function SaveAsTemplate() { let savedReferences = null; if (gMsgCompose && gMsgCompose.compFields) { // Clear References header. When we use the template, we don't want that // header, yet, "edit as new message" maintains it. So we need to clear // it when saving the template. // Note: The In-Reply-To header is the last entry in the references header, // so it will get cleared as well. savedReferences = gMsgCompose.compFields.references; gMsgCompose.compFields.references = null; } GenericSendMessage(nsIMsgCompDeliverMode.SaveAsTemplate); defaultSaveOperation = "template"; if (savedReferences) gMsgCompose.compFields.references = savedReferences; gAutoSaveKickedIn = false; gEditingDraft = false; } // Sets the additional FCC, in addition to the default FCC. function MessageFcc(aFolder) { if (!gMsgCompose) return; var msgCompFields = gMsgCompose.compFields; if (!msgCompFields) return; // Get the uri for the folder to FCC into. var fccURI = aFolder.URI; msgCompFields.fcc2 = (msgCompFields.fcc2 == fccURI) ? "nocopy://" : fccURI; } function updatePriorityMenu(priorityMenu) { var priority = (gMsgCompose && gMsgCompose.compFields && gMsgCompose.compFields.priority) || "Normal"; priorityMenu.getElementsByAttribute("value", priority)[0].setAttribute("checked", "true"); } function PriorityMenuSelect(target) { if (gMsgCompose) { var msgCompFields = gMsgCompose.compFields; if (msgCompFields) msgCompFields.priority = target.getAttribute("value"); } } function OutputFormatMenuSelect(target) { if (gMsgCompose) { var msgCompFields = gMsgCompose.compFields; var toolbar = document.getElementById("FormatToolbar"); var format_menubar = document.getElementById("formatMenu"); var insert_menubar = document.getElementById("insertMenu"); if (msgCompFields) switch (target.getAttribute('id')) { case "format_auto": gSendFormat = nsIMsgCompSendFormat.AskUser; break; case "format_plain": gSendFormat = nsIMsgCompSendFormat.PlainText; break; case "format_html": gSendFormat = nsIMsgCompSendFormat.HTML; break; case "format_both": gSendFormat = nsIMsgCompSendFormat.Both; break; } gHideMenus = (gSendFormat == nsIMsgCompSendFormat.PlainText); format_menubar.hidden = gHideMenus; insert_menubar.hidden = gHideMenus; if (gHideMenus) { gFormatToolbarHidden = toolbar.hidden; toolbar.hidden = true; toolbar.setAttribute("hideinmenu", "true"); } else { toolbar.hidden = gFormatToolbarHidden; toolbar.removeAttribute("hideinmenu"); } } } function SelectAddress() { var msgCompFields = gMsgCompose.compFields; Recipients2CompFields(msgCompFields); var toAddress = msgCompFields.to; var ccAddress = msgCompFields.cc; var bccAddress = msgCompFields.bcc; dump("toAddress: " + toAddress + "\n"); window.openDialog("chrome://messenger/content/addressbook/abSelectAddressesDialog.xul", "", "chrome,resizable,titlebar,modal", {composeWindow:top.window, msgCompFields:msgCompFields, toAddress:toAddress, ccAddress:ccAddress, bccAddress:bccAddress}); // We have to set focus to the addressingwidget because we seem to loose focus often // after opening the SelectAddresses Dialog- bug # 89950 AdjustFocus(); } // walk through the recipients list and add them to the inline spell checker ignore list function addRecipientsToIgnoreList(aAddressesToAdd) { if (InlineSpellCheckerUI.enabled) { // break the list of potentially many recipients back into individual names var emailAddresses = {}; var names = {}; var fullNames = {}; var numAddresses = MailServices.headerParser.parseHeadersWithArray(aAddressesToAdd, emailAddresses, names, fullNames); var tokenizedNames = []; // each name could consist of multiple words delimited by commas and/or spaces. // i.e. Green Lantern or Lantern,Green. for (let i = 0; i < names.value.length; i++) { if (!names.value[i]) continue; var splitNames = names.value[i].match(/[^\s,]+/g); if (splitNames) tokenizedNames = tokenizedNames.concat(splitNames); } if (InlineSpellCheckerUI.mInlineSpellChecker.spellCheckPending) { // spellchecker is enabled, but we must wait for its init to complete Services.obs.addObserver(function observe(subject, topic, data) { if (subject == gMsgCompose.editor) { Services.obs.removeObserver(observe, topic); InlineSpellCheckerUI.mInlineSpellChecker.ignoreWords(tokenizedNames); } }, "inlineSpellChecker-spellCheck-ended"); } else { InlineSpellCheckerUI.mInlineSpellChecker.ignoreWords(tokenizedNames); } } } function onAddressColCommand(aWidgetId) { gContentChanged = true; awSetAutoComplete(aWidgetId.slice(aWidgetId.lastIndexOf('#') + 1)); updateSendCommands(true); } /** * Called if the list of recipients changed in any way. * * @param aAutomatic Set to true if the change of recipients was invoked * programatically and should not be considered a change * of message content. */ function onRecipientsChanged(aAutomatic) { if (!aAutomatic) { gContentChanged = true; setupAutocomplete(); } updateSendCommands(true); } function InitLanguageMenu() { var languageMenuList = document.getElementById("languageMenuList"); if (!languageMenuList) return; var spellChecker = Cc["@mozilla.org/spellchecker/engine;1"] .getService(mozISpellCheckingEngine); // Get the list of dictionaries from the spellchecker. var dictList = spellChecker.getDictionaryList(); var count = dictList.length; // If dictionary count hasn't changed then no need to update the menu. if (sDictCount == count) return; // Store current dictionary count. sDictCount = count; // Load the language string bundle that will help us map // RFC 1766 strings to UI strings. var languageBundle = document.getElementById("languageBundle"); var isoStrArray; var langId; var langLabel; for (let i = 0; i < count; i++) { try { langId = dictList[i]; isoStrArray = dictList[i].split(/[-_]/); if (languageBundle && isoStrArray[0]) langLabel = languageBundle.getString(isoStrArray[0].toLowerCase()); // the user needs to be able to distinguish between the UK English dictionary // and say the United States English Dictionary. If we have a isoStr value then // wrap it in parentheses and append it to the menu item string. i.e. // English (US) and English (UK) if (!langLabel) langLabel = langId; // if we have a language ID like US or UK, append it to the menu item, and any sub-variety else if (isoStrArray.length > 1 && isoStrArray[1]) { langLabel += ' (' + isoStrArray[1]; if (isoStrArray.length > 2 && isoStrArray[2]) langLabel += '-' + isoStrArray[2]; langLabel += ')'; } } catch (ex) { // getString throws an exception when a key is not found in the // bundle. In that case, just use the original dictList string. langLabel = langId; } dictList[i] = [langLabel, langId]; } // sort by locale-aware collation dictList.sort( function compareFn(a, b) { return a[0].localeCompare(b[0]); } ); // Remove any languages from the list. while (languageMenuList.hasChildNodes()) languageMenuList.lastChild.remove(); for (let i = 0; i < count; i++) { var item = document.createElement("menuitem"); item.setAttribute("label", dictList[i][0]); item.setAttribute("value", dictList[i][1]); item.setAttribute("type", "radio"); languageMenuList.appendChild(item); } } function OnShowDictionaryMenu(aTarget) { InitLanguageMenu(); var spellChecker = InlineSpellCheckerUI.mInlineSpellChecker.spellChecker; var curLang = spellChecker.GetCurrentDictionary(); var languages = aTarget.getElementsByAttribute("value", curLang); if (languages.length > 0) languages[0].setAttribute("checked", true); } function ChangeLanguage(event) { // We need to change the dictionary language and if we are using inline spell check, // recheck the message var spellChecker = InlineSpellCheckerUI.mInlineSpellChecker.spellChecker; if (spellChecker.GetCurrentDictionary() != event.target.value) { spellChecker.SetCurrentDictionary(event.target.value); ComposeChangeLanguage(event.target.value) } event.stopPropagation(); } function ComposeChangeLanguage(aLang) { if (document.documentElement.getAttribute("lang") != aLang) { // Update the document language as well. // This is needed to synchronize the subject. document.documentElement.setAttribute("lang", aLang); // Update spellchecker pref Services.prefs.setCharPref("spellchecker.dictionary", aLang); // Now check the document and the subject over again with the new // dictionary. if (InlineSpellCheckerUI.enabled) { InlineSpellCheckerUI.mInlineSpellChecker.spellCheckRange(null); // Also force a recheck of the subject. The spell checker for the subject // isn't always ready yet. Usually throws unless the subject was selected // at least once. So don't auto-create it, hence pass 'false'. let inlineSpellChecker = GetMsgSubjectElement().editor.getInlineSpellChecker(false); if (inlineSpellChecker) { inlineSpellChecker.spellCheckRange(null); } } } } function ToggleReturnReceipt(target) { var msgCompFields = gMsgCompose.compFields; if (msgCompFields) { msgCompFields.returnReceipt = ! msgCompFields.returnReceipt; target.setAttribute('checked', msgCompFields.returnReceipt); gReceiptOptionChanged = true; } } function ToggleDSN(target) { var msgCompFields = gMsgCompose.compFields; if (msgCompFields) { msgCompFields.DSN = !msgCompFields.DSN; target.setAttribute('checked', msgCompFields.DSN); gDSNOptionChanged = true; } } function ToggleAttachVCard(target) { var msgCompFields = gMsgCompose.compFields; if (msgCompFields) { msgCompFields.attachVCard = ! msgCompFields.attachVCard; target.setAttribute('checked', msgCompFields.attachVCard); gAttachVCardOptionChanged = true; } } function FillIdentityList(menulist) { var accounts = FolderUtils.allAccountsSorted(true); for (let acc = 0; acc < accounts.length; acc++) { let account = accounts[acc]; let identities = account.identities; if (identities.length == 0) continue; for (let i = 0; i < identities.length; i++) { let identity = identities[i]; let item = menulist.appendItem(identity.identityName, identity.fullAddress, account.incomingServer.prettyName); item.setAttribute("identitykey", identity.key); item.setAttribute("accountkey", account.key); if (i == 0) { // Mark the first identity as default. item.setAttribute("default", "true"); } } } } function getCurrentAccountKey() { // get the accounts key var identityList = GetMsgIdentityElement(); return identityList.selectedItem.getAttribute("accountkey"); } function getCurrentIdentityKey() { // get the identity key var identityList = GetMsgIdentityElement(); return identityList.selectedItem.getAttribute("identitykey"); } function getIdentityForKey(key) { return MailServices.accounts.getIdentity(key); } function getCurrentIdentity() { return getIdentityForKey(getCurrentIdentityKey()); } function AdjustFocus() { let element = awGetInputElement(awGetNumberOfRecipients()); if (element.value == "") { awSetFocusTo(element); } else { element = GetMsgSubjectElement(); if (element.value == "") { element.focus(); } else { SetMsgBodyFrameFocus(); } } } function SetComposeWindowTitle() { var newTitle = GetMsgSubjectElement().value; if (newTitle == "" ) newTitle = sComposeMsgsBundle.getString("defaultSubject"); newTitle += GetCharsetUIString(); document.title = sComposeMsgsBundle.getString("windowTitlePrefix") + " " + newTitle; } // Check for changes to document and allow saving before closing // This is hooked up to the OS's window close widget (e.g., "X" for Windows) function ComposeCanClose() { if (gSendOrSaveOperationInProgress) { var brandShortName = sBrandBundle.getString("brandShortName"); var promptTitle = sComposeMsgsBundle.getString("quitComposeWindowTitle"); var promptMsg = sComposeMsgsBundle.getFormattedString("quitComposeWindowMessage2", [brandShortName], 1); var quitButtonLabel = sComposeMsgsBundle.getString("quitComposeWindowQuitButtonLabel2"); var waitButtonLabel = sComposeMsgsBundle.getString("quitComposeWindowWaitButtonLabel2"); if (Services.prompt.confirmEx(window, promptTitle, promptMsg, (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1), waitButtonLabel, quitButtonLabel, null, null, {value:0}) == 1) { gMsgCompose.abort(); return true; } return false; } // Returns FALSE only if user cancels save action if (gContentChanged || gMsgCompose.bodyModified || (gAutoSaveKickedIn && !gEditingDraft)) { // call window.focus, since we need to pop up a dialog // and therefore need to be visible (to prevent user confusion) window.focus(); let draftFolderURI = gCurrentIdentity.draftFolder; let draftFolderName = MailUtils.getFolderForURI(draftFolderURI).prettyName; switch (Services.prompt.confirmEx(window, sComposeMsgsBundle.getString("saveDlogTitle"), sComposeMsgsBundle.getFormattedString("saveDlogMessages3", [draftFolderName]), (Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0) + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2), null, null, sComposeMsgsBundle.getString("discardButtonLabel"), null, {value:0})) { case 0: //Save // we can close immediately if we already autosaved the draft if (!gContentChanged && !gMsgCompose.bodyModified) break; gCloseWindowAfterSave = true; GenericSendMessage(nsIMsgCompDeliverMode.AutoSaveAsDraft); return false; case 1: //Cancel return false; case 2: //Don't Save // only delete the draft if we didn't start off editing a draft if (!gEditingDraft && gAutoSaveKickedIn) RemoveDraft(); break; } } return true; } function RemoveDraft() { try { var draftId = gMsgCompose.compFields.draftId; var msgKey = draftId.substr(draftId.indexOf('#') + 1); var folder = sRDF.GetResource(gMsgCompose.savedFolderURI); try { if (folder instanceof Ci.nsIMsgFolder) { let msg = folder.GetMessageHeader(msgKey); folder.deleteMessages([msg], null, true, false, null, false); } } catch (ex) // couldn't find header - perhaps an imap folder. { if (folder instanceof Ci.nsIMsgImapMailFolder) { const kImapMsgDeletedFlag = 0x0008; folder.storeImapFlags(kImapMsgDeletedFlag, true, [msgKey], null); } } } catch (ex) {} } function SetContentAndBodyAsUnmodified() { gMsgCompose.bodyModified = false; gContentChanged = false; } function MsgComposeCloseWindow() { if (gMsgCompose) gMsgCompose.CloseWindow(); else window.close(); } // attachedLocalFile must be a nsIFile function SetLastAttachDirectory(attachedLocalFile) { try { var file = attachedLocalFile.QueryInterface(Ci.nsIFile); var parent = file.parent.QueryInterface(Ci.nsIFile); Services.prefs.setComplexValue(kComposeAttachDirPrefName, Ci.nsIFile, parent); } catch (ex) { dump("error: SetLastAttachDirectory failed: " + ex + "\n"); } } function AttachFile() { //Get file using nsIFilePicker and convert to URL const nsIFilePicker = Ci.nsIFilePicker; let fp = Cc["@mozilla.org/filepicker;1"] .createInstance(nsIFilePicker); fp.init(window, sComposeMsgsBundle.getString("chooseFileToAttach"), nsIFilePicker.modeOpenMultiple); let lastDirectory = GetLocalFilePref(kComposeAttachDirPrefName); if (lastDirectory) fp.displayDirectory = lastDirectory; fp.appendFilters(nsIFilePicker.filterAll); fp.open(rv => { if (rv != nsIFilePicker.returnOK || !fp.files) { return; } try { let firstAttachedFile = AttachFiles(fp.files); if (firstAttachedFile) { SetLastAttachDirectory(firstAttachedFile); } } catch (ex) { dump("failed to get attachments: " + ex + "\n"); } }); } function AttachFiles(attachments) { if (!attachments || !attachments.hasMoreElements()) return null; var firstAttachedFile = null; while (attachments.hasMoreElements()) { var currentFile = attachments.getNext().QueryInterface(Ci.nsIFile); if (!firstAttachedFile) { firstAttachedFile = currentFile; } var fileHandler = Services.io.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler); var currentAttachment = fileHandler.getURLSpecFromFile(currentFile); if (!DuplicateFileCheck(currentAttachment)) { var attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment); attachment.url = currentAttachment; attachment.size = currentFile.fileSize; AddAttachment(attachment); gContentChanged = true; } } return firstAttachedFile; } function AddAttachment(attachment) { if (attachment && attachment.url) { var bucket = GetMsgAttachmentElement(); var item = document.createElement("listitem"); if (!attachment.name) attachment.name = gMsgCompose.AttachmentPrettyName(attachment.url, attachment.urlCharset); // for security reasons, don't allow *-message:// uris to leak out // we don't want to reveal the .slt path (for mailbox://), or the username or hostname var messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i; if (messagePrefix.test(attachment.name)) attachment.name = sComposeMsgsBundle.getString("messageAttachmentSafeName"); else { // for security reasons, don't allow mail protocol uris to leak out // we don't want to reveal the .slt path (for mailbox://), or the username or hostname var mailProtocol = /^file:|^mailbox:|^imap:|^s?news:/i; if (mailProtocol.test(attachment.name)) attachment.name = sComposeMsgsBundle.getString("partAttachmentSafeName"); } var nameAndSize = attachment.name; if (attachment.size != -1) nameAndSize += " (" + gMessenger.formatFileSize(attachment.size) + ")"; item.setAttribute("label", nameAndSize); //use for display only item.attachment = attachment; //full attachment object stored here try { item.setAttribute("tooltiptext", decodeURI(attachment.url)); } catch(e) { item.setAttribute("tooltiptext", attachment.url); } item.setAttribute("class", "listitem-iconic"); item.setAttribute("image", "moz-icon:" + attachment.url); item.setAttribute("crop", "center"); bucket.appendChild(item); } } function SelectAllAttachments() { var bucketList = GetMsgAttachmentElement(); if (bucketList) bucketList.selectAll(); } function MessageHasAttachments() { var bucketList = GetMsgAttachmentElement(); if (bucketList) { return (bucketList && bucketList.hasChildNodes() && (bucketList == top.document.commandDispatcher.focusedElement)); } return false; } function MessageGetNumSelectedAttachments() { var bucketList = GetMsgAttachmentElement(); return (bucketList) ? bucketList.selectedItems.length : 0; } function AttachPage() { var params = { action: "5", url: null }; window.openDialog("chrome://communicator/content/openLocation.xul", "_blank", "chrome,close,titlebar,modal", params); if (params.url) { var attachment = Cc["@mozilla.org/messengercompose/attachment;1"] .createInstance(Ci.nsIMsgAttachment); attachment.url = params.url; AddAttachment(attachment); } } function DuplicateFileCheck(FileUrl) { var bucket = GetMsgAttachmentElement(); for (let i = 0; i < bucket.childNodes.length; i++) { let attachment = bucket.childNodes[i].attachment; if (attachment) { if (FileUrl == attachment.url) return true; } } return false; } function Attachments2CompFields(compFields) { var bucket = GetMsgAttachmentElement(); //First, we need to clear all attachment in the compose fields compFields.removeAttachments(); for (let i = 0; i < bucket.childNodes.length; i++) { let attachment = bucket.childNodes[i].attachment; if (attachment) compFields.addAttachment(attachment); } } function RemoveAllAttachments() { var child; var bucket = GetMsgAttachmentElement(); while (bucket.hasChildNodes()) { child = bucket.removeChild(bucket.lastChild); // Let's release the attachment object hold by the node else it won't go away until the window is destroyed child.attachment = null; } } function RemoveSelectedAttachment() { var child; var bucket = GetMsgAttachmentElement(); if (bucket.selectedItems.length > 0) { for (let i = bucket.selectedItems.length - 1; i >= 0; i--) { child = bucket.removeChild(bucket.selectedItems[i]); // Let's release the attachment object hold by the node else it won't go away until the window is destroyed child.attachment = null; } gContentChanged = true; } } function RenameSelectedAttachment() { var bucket = GetMsgAttachmentElement(); if (bucket.selectedItems.length != 1) return; // not one attachment selected var item = bucket.getSelectedItem(0); var attachmentName = {value: item.attachment.name}; if (Services.prompt.prompt( window, sComposeMsgsBundle.getString("renameAttachmentTitle"), sComposeMsgsBundle.getString("renameAttachmentMessage"), attachmentName, null, {value: 0})) { var modifiedAttachmentName = attachmentName.value; if (modifiedAttachmentName == "") return; // name was not filled, bail out var nameAndSize = modifiedAttachmentName; if (item.attachment.size != -1) nameAndSize += " (" + gMessenger.formatFileSize(item.attachment.size) + ")"; item.label = nameAndSize; item.attachment.name = modifiedAttachmentName; gContentChanged = true; } } function FocusOnFirstAttachment() { var bucketList = GetMsgAttachmentElement(); if (bucketList && bucketList.hasChildNodes()) bucketList.selectItem(bucketList.firstChild); } function AttachmentElementHasItems() { var element = GetMsgAttachmentElement(); return element ? element.childNodes.length : 0; } function OpenSelectedAttachment() { let bucket = document.getElementById("attachmentBucket"); if (bucket.selectedItems.length == 1) { let attachmentUrl = bucket.getSelectedItem(0).attachment.url; let messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i; if (messagePrefix.test(attachmentUrl)) { // We must be dealing with a forwarded attachment, treat this special. let msgHdr = gMessenger.msgHdrFromURI(attachmentUrl); if (msgHdr) { MailUtils.openMessageInNewWindow(msgHdr); } } else { // Turn the URL into a nsIURI object then open it. let uri = Services.io.newURI(attachmentUrl); if (uri) { let channel = Services.io.newChannelFromURI(uri, null, Services.scriptSecurityManager.getSystemPrincipal(), null, Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, Ci.nsIContentPolicy.TYPE_OTHER); if (channel) { let uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); uriLoader.openURI(channel, true, new nsAttachmentOpener()); } } } } // if one attachment selected } function nsAttachmentOpener() { } nsAttachmentOpener.prototype = { QueryInterface: ChromeUtils.generateQI([Ci.nsIURIContentListener, Ci.nsIInterfaceRequestor]), doContent: function(contentType, isContentPreferred, request, contentHandler) { return false; }, isPreferred: function(contentType, desiredContentType) { return false; }, canHandleContent: function(contentType, isContentPreferred, desiredContentType) { return false; }, getInterface: function(iid) { if (iid.equals(Ci.nsIDOMWindow)) { return window; } if (iid.equals(Ci.nsIDocShell)) { return window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell); } return this.QueryInterface(iid); }, loadCookie: null, parentContentListener: null } function DetermineHTMLAction(convertible) { try { gMsgCompose.expandMailingLists(); } catch(ex) { dump("gMsgCompose.expandMailingLists failed: " + ex + "\n"); } if (!gMsgCompose.composeHTML) { return nsIMsgCompSendFormat.PlainText; } if (gSendFormat == nsIMsgCompSendFormat.AskUser) { return gMsgCompose.determineHTMLAction(convertible); } return gSendFormat; } function DetermineConvertibility() { if (!gMsgCompose.composeHTML) return nsIMsgCompConvertible.Plain; try { return gMsgCompose.bodyConvertible(); } catch(ex) {} return nsIMsgCompConvertible.No; } function LoadIdentity(startup) { var identityElement = GetMsgIdentityElement(); var prevIdentity = gCurrentIdentity; if (identityElement) { identityElement.value = identityElement.selectedItem.value; var idKey = identityElement.selectedItem.getAttribute("identitykey"); gCurrentIdentity = MailServices.accounts.getIdentity(idKey); let accountKey = null; if (identityElement.selectedItem) accountKey = identityElement.selectedItem.getAttribute("accountkey"); let maxRecipients = awGetMaxRecipients(); for (let i = 1; i <= maxRecipients; i++) { let params = JSON.parse(awGetInputElement(i).searchParam); params.idKey = idKey; params.accountKey = accountKey; awGetInputElement(i).searchParam = JSON.stringify(params); } if (!startup && prevIdentity && idKey != prevIdentity.key) { var prevReplyTo = prevIdentity.replyTo; var prevCc = ""; var prevBcc = ""; var prevReceipt = prevIdentity.requestReturnReceipt; var prevDSN = prevIdentity.requestDSN; var prevAttachVCard = prevIdentity.attachVCard; if (prevIdentity.doCc) prevCc += prevIdentity.doCcList; if (prevIdentity.doBcc) prevBcc += prevIdentity.doBccList; var newReplyTo = gCurrentIdentity.replyTo; var newCc = ""; var newBcc = ""; var newReceipt = gCurrentIdentity.requestReturnReceipt; var newDSN = gCurrentIdentity.requestDSN; var newAttachVCard = gCurrentIdentity.attachVCard; if (gCurrentIdentity.doCc) newCc += gCurrentIdentity.doCcList; if (gCurrentIdentity.doBcc) newBcc += gCurrentIdentity.doBccList; var needToCleanUp = false; var msgCompFields = gMsgCompose.compFields; if (!gReceiptOptionChanged && prevReceipt == msgCompFields.returnReceipt && prevReceipt != newReceipt) { msgCompFields.returnReceipt = newReceipt; document.getElementById("returnReceiptMenu").setAttribute('checked',msgCompFields.returnReceipt); } if (!gDSNOptionChanged && prevDSN == msgCompFields.DSN && prevDSN != newDSN) { msgCompFields.DSN = newDSN; document.getElementById("dsnMenu").setAttribute('checked',msgCompFields.DSN); } if (!gAttachVCardOptionChanged && prevAttachVCard == msgCompFields.attachVCard && prevAttachVCard != newAttachVCard) { msgCompFields.attachVCard = newAttachVCard; document.getElementById("cmd_attachVCard").setAttribute('checked',msgCompFields.attachVCard); } if (newReplyTo != prevReplyTo) { needToCleanUp = true; if (prevReplyTo != "") awRemoveRecipients(msgCompFields, "addr_reply", prevReplyTo); if (newReplyTo != "") awAddRecipients(msgCompFields, "addr_reply", newReplyTo); } let toAddrs = new Set(msgCompFields.splitRecipients(msgCompFields.to, true)); let ccAddrs = new Set(msgCompFields.splitRecipients(msgCompFields.cc, true)); if (newCc != prevCc) { needToCleanUp = true; if (prevCc) awRemoveRecipients(msgCompFields, "addr_cc", prevCc); if (newCc) { // Ensure none of the Ccs are already in To. let cc2 = msgCompFields.splitRecipients(newCc, true); newCc = cc2.filter(x => !toAddrs.has(x)).join(", "); awAddRecipients(msgCompFields, "addr_cc", newCc); } } if (newBcc != prevBcc) { needToCleanUp = true; if (prevBcc) awRemoveRecipients(msgCompFields, "addr_bcc", prevBcc); if (newBcc) { // Ensure none of the Bccs are already in To or Cc. let bcc2 = msgCompFields.splitRecipients(newBcc, true); let toCcAddrs = new Set([...toAddrs, ...ccAddrs]); newBcc = bcc2.filter(x => !toCcAddrs.has(x)).join(", "); awAddRecipients(msgCompFields, "addr_bcc", newBcc); } } if (needToCleanUp) awCleanupRows(); try { gMsgCompose.identity = gCurrentIdentity; } catch (ex) { dump("### Cannot change the identity: " + ex + "\n");} var event = document.createEvent('Events'); event.initEvent('compose-from-changed', false, true); document.getElementById("msgcomposeWindow").dispatchEvent(event); gComposeNotificationBar.clearIdentityWarning(); } if (!startup) { if (Services.prefs.getBoolPref("mail.autoComplete.highlightNonMatches")) document.getElementById('addressCol2#1').highlightNonMatches = true; // Only do this if we aren't starting up... // It gets done as part of startup already. addRecipientsToIgnoreList(gCurrentIdentity.fullAddress); } } } function setupAutocomplete() { var autoCompleteWidget = document.getElementById("addressCol2#1"); // if the pref is set to turn on the comment column, honor it here. // this element then gets cloned for subsequent rows, so they should // honor it as well // if (Services.prefs.getBoolPref("mail.autoComplete.highlightNonMatches")) autoCompleteWidget.highlightNonMatches = true; if (Services.prefs.getIntPref("mail.autoComplete.commentColumn", 0) != 0) autoCompleteWidget.showCommentColumn = true; } function subjectKeyPress(event) { switch(event.keyCode) { case KeyEvent.DOM_VK_TAB: if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { SetMsgBodyFrameFocus(); event.preventDefault(); } break; case KeyEvent.DOM_VK_RETURN: SetMsgBodyFrameFocus(); break; } } function AttachmentBucketClicked(event) { if (event.button != 0) return; if (event.originalTarget.localName == "listboxbody") goDoCommand('cmd_attachFile'); else if (event.originalTarget.localName == "listitem" && event.detail == 2) OpenSelectedAttachment(); } // Content types supported in the attachmentBucketObserver. let flavours = [ "text/x-moz-message", "application/x-moz-file", "text/x-moz-url", ]; var attachmentBucketObserver = { onDrop(aEvent) { let dt = aEvent.dataTransfer; let dataList = []; for (let i = 0; i < dt.mozItemCount; i++) { let types = Array.from(dt.mozTypesAt(i)); for (let flavour of flavours) { if (types.includes(flavour)) { let data = dt.mozGetDataAt(flavour, i); if (data) { dataList.push({ data, flavour }); } break; } } } for (let { data, flavour } of dataList) { let isValidAttachment = false; let prettyName; let size; // We could be dropping an attachment of various flavours; // check and do the right thing. switch (flavour) { case "application/x-moz-file": { if (data instanceof Ci.nsIFile) { size = data.fileSize; } try { data = Services.io.getProtocolHandler("file") .QueryInterface(Ci.nsIFileProtocolHandler) .getURLSpecFromFile(data); isValidAttachment = true; } catch (e) { Cu.reportError("Couldn't process the dragged file " + data.leafName + ":" + e); } break; } case "text/x-moz-message": { isValidAttachment = true; let msgHdr = gMessenger.messageServiceFromURI(data) .messageURIToMsgHdr(data); prettyName = msgHdr.mime2DecodedSubject + ".eml"; size = msgHdr.messageSize; break; } case "text/x-moz-url": { let pieces = data.split("\n"); data = pieces[0]; if (pieces.length > 1) { prettyName = pieces[1]; } if (pieces.length > 2) { size = parseInt(pieces[2]); } // If this is a URL (or selected text), check if it's a valid URL // by checking if we can extract a scheme using Services.io. // Don't attach invalid or mailto: URLs. try { let scheme = Services.io.extractScheme(data); if (scheme != "mailto") { isValidAttachment = true; } } catch (ex) {} break; } } if (isValidAttachment && !DuplicateFileCheck(data)) { let attachment = Cc["@mozilla.org/messengercompose/attachment;1"] .createInstance(Ci.nsIMsgAttachment); attachment.url = data; attachment.name = prettyName; if (size !== undefined) { attachment.size = size; } AddAttachment(attachment); } } aEvent.stopPropagation(); }, onDragOver(aEvent) { let dragSession = Cc["@mozilla.org/widget/dragservice;1"] .getService(Ci.nsIDragService).getCurrentSession(); for (let flavour of flavours) { if (dragSession.isDataFlavorSupported(flavour)) { let attachmentBucket = GetMsgAttachmentElement(); attachmentBucket.setAttribute("dragover", "true"); aEvent.stopPropagation(); aEvent.preventDefault(); break; } } }, onDragExit(aEvent) { let attachmentBucket = GetMsgAttachmentElement(); attachmentBucket.removeAttribute("dragover"); }, }; function DisplaySaveFolderDlg(folderURI) { try { var showDialog = gCurrentIdentity.showSaveMsgDlg; } catch (e) { return; } if (showDialog){ let msgfolder = MailUtils.getFolderForURI(folderURI, true); if (!msgfolder) return; var checkbox = {value:0}; var SaveDlgTitle = sComposeMsgsBundle.getString("SaveDialogTitle"); var dlgMsg = sComposeMsgsBundle.getFormattedString("SaveDialogMsg", [msgfolder.name, msgfolder.server.prettyName]); var CheckMsg = sComposeMsgsBundle.getString("CheckMsg"); Services.prompt.alertCheck(window, SaveDlgTitle, dlgMsg, CheckMsg, checkbox); try { gCurrentIdentity.showSaveMsgDlg = !checkbox.value; }//try catch (e) { return; }//catch }//if return; } function SetMsgAddressingWidgetElementFocus() { awSetFocusTo(awGetInputElement(awGetNumberOfRecipients())); } function SetMsgIdentityElementFocus() { GetMsgIdentityElement().focus(); } function SetMsgSubjectElementFocus() { GetMsgSubjectElement().focus(); } function SetMsgAttachmentElementFocus() { GetMsgAttachmentElement().focus(); FocusOnFirstAttachment(); } function SetMsgBodyFrameFocus() { //window.content.focus(); fails to blur the currently focused element document.commandDispatcher .advanceFocusIntoSubtree(document.getElementById("appcontent")); } function GetMsgAddressingWidgetElement() { if (!gMsgAddressingWidgetElement) gMsgAddressingWidgetElement = document.getElementById("addressingWidget"); return gMsgAddressingWidgetElement; } function GetMsgIdentityElement() { if (!gMsgIdentityElement) gMsgIdentityElement = document.getElementById("msgIdentity"); return gMsgIdentityElement; } function GetMsgSubjectElement() { if (!gMsgSubjectElement) gMsgSubjectElement = document.getElementById("msgSubject"); return gMsgSubjectElement; } function GetMsgAttachmentElement() { if (!gMsgAttachmentElement) gMsgAttachmentElement = document.getElementById("attachmentBucket"); return gMsgAttachmentElement; } function GetMsgHeadersToolbarElement() { if (!gMsgHeadersToolbarElement) gMsgHeadersToolbarElement = document.getElementById("MsgHeadersToolbar"); return gMsgHeadersToolbarElement; } function IsMsgHeadersToolbarCollapsed() { var element = GetMsgHeadersToolbarElement(); return element && element.collapsed; } function WhichElementHasFocus() { var msgIdentityElement = GetMsgIdentityElement(); var msgAddressingWidgetElement = GetMsgAddressingWidgetElement(); var msgSubjectElement = GetMsgSubjectElement(); var msgAttachmentElement = GetMsgAttachmentElement(); if (top.document.commandDispatcher.focusedWindow == content) return content; var currentNode = top.document.commandDispatcher.focusedElement; while (currentNode) { if (currentNode == msgIdentityElement || currentNode == msgAddressingWidgetElement || currentNode == msgSubjectElement || currentNode == msgAttachmentElement) return currentNode; currentNode = currentNode.parentNode; } return null; } // Function that performs the logic of switching focus from // one element to another in the mail compose window. // The default element to switch to when going in either // direction (shift or no shift key pressed), is the // AddressingWidgetElement. // // The only exception is when the MsgHeadersToolbar is // collapsed, then the focus will always be on the body of // the message. function SwitchElementFocus(event) { var focusedElement = WhichElementHasFocus(); if (event && event.shiftKey) { if (IsMsgHeadersToolbarCollapsed()) SetMsgBodyFrameFocus(); else if (focusedElement == gMsgAddressingWidgetElement) SetMsgIdentityElementFocus(); else if (focusedElement == gMsgIdentityElement) SetMsgBodyFrameFocus(); else if (focusedElement == content) { // only set focus to the attachment element if there // are any attachments. if (AttachmentElementHasItems()) SetMsgAttachmentElementFocus(); else SetMsgSubjectElementFocus(); } else if (focusedElement == gMsgAttachmentElement) SetMsgSubjectElementFocus(); else SetMsgAddressingWidgetElementFocus(); } else { if (IsMsgHeadersToolbarCollapsed()) SetMsgBodyFrameFocus(); else if (focusedElement == gMsgAddressingWidgetElement) SetMsgSubjectElementFocus(); else if (focusedElement == gMsgSubjectElement) { // only set focus to the attachment element if there // are any attachments. if (AttachmentElementHasItems()) SetMsgAttachmentElementFocus(); else SetMsgBodyFrameFocus(); } else if (focusedElement == gMsgAttachmentElement) SetMsgBodyFrameFocus(); else if (focusedElement == content) SetMsgIdentityElementFocus(); else SetMsgAddressingWidgetElementFocus(); } } function loadHTMLMsgPrefs() { var fontFace = Services.prefs.getStringPref("msgcompose.font_face", ""); doStatefulCommand("cmd_fontFace", fontFace); var fontSize = Services.prefs.getCharPref("msgcompose.font_size", ""); if (fontSize) EditorSetFontSize(fontSize); var bodyElement = GetBodyElement(); var textColor = Services.prefs.getCharPref("msgcompose.text_color", ""); if (!bodyElement.hasAttribute("text") && textColor) { bodyElement.setAttribute("text", textColor); gDefaultTextColor = textColor; document.getElementById("cmd_fontColor").setAttribute("state", textColor); onFontColorChange(); } var bgColor = Services.prefs.getCharPref("msgcompose.background_color", ""); if (!bodyElement.hasAttribute("bgcolor") && bgColor) { bodyElement.setAttribute("bgcolor", bgColor); gDefaultBackgroundColor = bgColor; document.getElementById("cmd_backgroundColor").setAttribute("state", bgColor); onBackgroundColorChange(); } } function AutoSave() { if (gMsgCompose.editor && (gContentChanged || gMsgCompose.bodyModified) && !gSendOrSaveOperationInProgress) { GenericSendMessage(nsIMsgCompDeliverMode.AutoSaveAsDraft); gAutoSaveKickedIn = true; } gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval); } /** * Helper function to remove a query part from a URL, so for example: * ...?remove=xx&other=yy becomes ...?other=yy. * * @param aURL the URL from which to remove the query part * @param aQuery the query part to remove * @return the URL with the query part removed */ function removeQueryPart(aURL, aQuery) { // Quick pre-check. if (!aURL.includes(aQuery)) return aURL; let indexQM = aURL.indexOf("?"); if (indexQM < 0) return aURL; let queryParts = aURL.substr(indexQM + 1).split("&"); let indexPart = queryParts.indexOf(aQuery); if (indexPart < 0) return aURL; queryParts.splice(indexPart, 1); return aURL.substr(0, indexQM + 1) + queryParts.join("&"); } function InitEditor(editor) { // Set the eEditorMailMask flag to avoid using content prefs for the spell // checker, otherwise the dictionary setting in preferences is ignored and // the dictionary is inconsistent between the subject and message body. var eEditorMailMask = Ci.nsIEditor.eEditorMailMask; editor.flags |= eEditorMailMask; GetMsgSubjectElement().editor.flags |= eEditorMailMask; // Control insertion of line breaks. editor.returnInParagraphCreatesNewParagraph = Services.prefs.getBoolPref("mail.compose.default_to_paragraph") || Services.prefs.getBoolPref("editor.CR_creates_new_p"); editor.document.execCommand("defaultparagraphseparator", false, gMsgCompose.composeHTML && Services.prefs.getBoolPref("mail.compose.default_to_paragraph") ? "p" : "br"); gMsgCompose.initEditor(editor, window.content); InlineSpellCheckerUI.init(editor); EnableInlineSpellCheck(Services.prefs.getBoolPref("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 Ci.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 = Cc["@mozilla.org/messenger;1"] .createInstance(Ci.nsIMessenger) .messageServiceFromURI(gOriginalMsgURI); let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI); if (src.startsWith(removeQueryPart(originalMsgNeckoURI.spec, "type=application/x-message-display"))) { // 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. Cu.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 = Cc["@mozilla.org/messenger;1"] .createInstance(Ci.nsIMessenger) .messageServiceFromURI(gOriginalMsgURI); let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI); if (background.startsWith( removeQueryPart(originalMsgNeckoURI.spec, "type=application/x-message-display"))) { try { editor.document.body.background = loadBlockedImage(background, true); } catch (e) { // Couldn't load the referenced image. Cu.reportError(e); } } } } /** * The event listener for the "spellcheck-changed" event updates * the document language. */ function updateDocumentLanguage(event) { document.documentElement.setAttribute("lang", event.detail.dictionary); } function EnableInlineSpellCheck(aEnableInlineSpellCheck) { InlineSpellCheckerUI.enabled = aEnableInlineSpellCheck; GetMsgSubjectElement().setAttribute("spellcheck", aEnableInlineSpellCheck); } function getMailToolbox() { return document.getElementById("compose-toolbox"); } function MailToolboxCustomizeInit() { if (document.commandDispatcher.focusedWindow == content) window.focus(); disableEditableFields(); GetMsgHeadersToolbarElement().setAttribute("moz-collapsed", true); document.getElementById("compose-toolbar-sizer").setAttribute("moz-collapsed", true); document.getElementById("content-frame").setAttribute("moz-collapsed", true); toolboxCustomizeInit("mail-menubar"); } function MailToolboxCustomizeDone(aToolboxChanged) { toolboxCustomizeDone("mail-menubar", getMailToolbox(), aToolboxChanged); GetMsgHeadersToolbarElement().removeAttribute("moz-collapsed"); document.getElementById("compose-toolbar-sizer").removeAttribute("moz-collapsed"); document.getElementById("content-frame").removeAttribute("moz-collapsed"); enableEditableFields(); SetMsgBodyFrameFocus(); } function MailToolboxCustomizeChange(aEvent) { toolboxCustomizeChange(getMailToolbox(), aEvent); } /** * 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"); }, clearBlockedContentNotification: function() { this.notificationBar.removeNotification( this.notificationBar.getNotificationWithValue("blockedContent")); }, clearNotifications: function(aValue) { this.notificationBar.removeAllNotifications(true); }, setIdentityWarning: function(aIdentityName) { if (!this.notificationBar.getNotificationWithValue("identityWarning")) { let text = sComposeMsgsBundle.getString("identityWarning").split("%S"); let label = new DocumentFragment(); label.appendChild(document.createTextNode(text[0])); label.appendChild(document.createElement("b")); label.lastChild.appendChild(document.createTextNode(aIdentityName)); label.appendChild(document.createTextNode(text[1])); this.notificationBar.appendNotification(label, "identityWarning", null, this.notificationBar.PRIORITY_WARNING_HIGH, null); } }, clearIdentityWarning: function() { let idWarning = this.notificationBar.getNotificationWithValue("identityWarning"); if (idWarning) this.notificationBar.removeNotification(idWarning); } }; /** * 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. Cu.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.clearBlockedContentNotification(); } 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 = Cc["@mozilla.org/mime;1"] .getService(Ci.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.newChannelFromURI(uri, null, Services.scriptSecurityManager.getSystemPrincipal(), null, Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, Ci.nsIContentPolicy.TYPE_OTHER); let inputStream = channel.open(); let stream = Cc["@mozilla.org/binaryinputstream;1"] .createInstance(Ci.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"); } } }