diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/mailnews/components/compose | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/mailnews/components/compose')
11 files changed, 6395 insertions, 0 deletions
diff --git a/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js b/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js new file mode 100644 index 0000000000..48f7f31b1a --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js @@ -0,0 +1,3936 @@ +/* -*- 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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +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 : function(iid) + { + if (iid.equals(Ci.nsIWebProgressListener) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_NOINTERFACE; + } +}; + +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.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, "&"); + 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: function(iid) + { + if (iid.equals(Ci.nsIURIContentListener) || + iid.equals(Ci.nsIInterfaceRequestor) || + iid.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_NOINTERFACE; + }, + + 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"); + } + } +} diff --git a/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js new file mode 100644 index 0000000000..38cd1f5ecc --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js @@ -0,0 +1,1167 @@ +/* -*- 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/. */ + +const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +top.MAX_RECIPIENTS = 1; /* for the initial listitem created in the XUL */ + +var inputElementType = ""; +var selectElementType = ""; +var selectElementIndexTable = null; + +var gNumberOfCols = 0; + +var gDragService = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService); + +/** + * global variable inherited from MsgComposeCommands.js + * + var gMsgCompose; + */ + +function awGetMaxRecipients() +{ + return top.MAX_RECIPIENTS; +} + +function awGetNumberOfCols() +{ + if (gNumberOfCols == 0) + { + var listbox = document.getElementById('addressingWidget'); + var listCols = listbox.getElementsByTagName('listcol'); + gNumberOfCols = listCols.length; + if (!gNumberOfCols) + gNumberOfCols = 1; /* if no cols defined, that means we have only one! */ + } + + return gNumberOfCols; +} + +function awInputElementName() +{ + if (inputElementType == "") + inputElementType = document.getElementById("addressCol2#1").localName; + return inputElementType; +} + +function awSelectElementName() +{ + if (selectElementType == "") + selectElementType = document.getElementById("addressCol1#1").localName; + return selectElementType; +} + +// TODO: replace awGetSelectItemIndex with recipient type index constants + +function awGetSelectItemIndex(itemData) +{ + if (selectElementIndexTable == null) + { + selectElementIndexTable = new Object(); + var selectElem = document.getElementById("addressCol1#1"); + for (var i = 0; i < selectElem.childNodes[0].childNodes.length; i ++) + { + var aData = selectElem.childNodes[0].childNodes[i].getAttribute("value"); + selectElementIndexTable[aData] = i; + } + } + return selectElementIndexTable[itemData]; +} + +function Recipients2CompFields(msgCompFields) +{ + if (!msgCompFields) { + throw new Error("Message Compose Error: msgCompFields is null (ExtractRecipients)"); + return; + } + + var i = 1; + var addrTo = ""; + var addrCc = ""; + var addrBcc = ""; + var addrReply = ""; + var addrNg = ""; + var addrFollow = ""; + var to_Sep = ""; + var cc_Sep = ""; + var bcc_Sep = ""; + var reply_Sep = ""; + var ng_Sep = ""; + var follow_Sep = ""; + + var recipientType; + var inputField; + var fieldValue; + var recipient; + while ((inputField = awGetInputElement(i))) + { + fieldValue = inputField.value; + + if (fieldValue != "") + { + recipientType = awGetPopupElement(i).value; + recipient = null; + + switch (recipientType) + { + case "addr_to" : + case "addr_cc" : + case "addr_bcc" : + case "addr_reply" : + try { + let headerParser = MailServices.headerParser; + recipient = + headerParser.makeFromDisplayAddress(fieldValue) + .map(fullValue => headerParser.makeMimeAddress( + fullValue.name, + fullValue.email)) + .join(", "); + } catch (ex) { + recipient = fieldValue; + } + break; + } + + switch (recipientType) + { + case "addr_to" : addrTo += to_Sep + recipient; to_Sep = ","; break; + case "addr_cc" : addrCc += cc_Sep + recipient; cc_Sep = ","; break; + case "addr_bcc" : addrBcc += bcc_Sep + recipient; bcc_Sep = ","; break; + case "addr_reply" : addrReply += reply_Sep + recipient; reply_Sep = ","; break; + case "addr_newsgroups" : addrNg += ng_Sep + fieldValue; ng_Sep = ","; break; + case "addr_followup" : addrFollow += follow_Sep + fieldValue; follow_Sep = ","; break; + case "addr_other": + let headerName = awGetPopupElement(i).label; + headerName = headerName.substring(0, headerName.indexOf(':')); + msgCompFields.setRawHeader(headerName, fieldValue, null); + break; + } + } + i ++; + } + + msgCompFields.to = addrTo; + msgCompFields.cc = addrCc; + msgCompFields.bcc = addrBcc; + msgCompFields.replyTo = addrReply; + msgCompFields.newsgroups = addrNg; + msgCompFields.followupTo = addrFollow; +} + +function CompFields2Recipients(msgCompFields) +{ + if (msgCompFields) { + var listbox = document.getElementById('addressingWidget'); + var newListBoxNode = listbox.cloneNode(false); + var listBoxColsClone = listbox.firstChild.cloneNode(true); + newListBoxNode.appendChild(listBoxColsClone); + let templateNode = listbox.querySelector("listitem"); + // dump("replacing child in comp fields 2 recips \n"); + listbox.parentNode.replaceChild(newListBoxNode, listbox); + + top.MAX_RECIPIENTS = 0; + var msgReplyTo = msgCompFields.replyTo; + var msgTo = msgCompFields.to; + var msgCC = msgCompFields.cc; + var msgBCC = msgCompFields.bcc; + var msgNewsgroups = msgCompFields.newsgroups; + var msgFollowupTo = msgCompFields.followupTo; + var havePrimaryRecipient = false; + if (msgReplyTo) + awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgReplyTo, false), + "addr_reply", newListBoxNode, templateNode); + if (msgTo) { + var rcp = msgCompFields.splitRecipients(msgTo, false); + if (rcp.length) + { + awSetInputAndPopupFromArray(rcp, "addr_to", newListBoxNode, templateNode); + havePrimaryRecipient = true; + } + } + if (msgCC) + awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgCC, false), + "addr_cc", newListBoxNode, templateNode); + if (msgBCC) + awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgBCC, false), + "addr_bcc", newListBoxNode, templateNode); + if (msgNewsgroups) { + awSetInputAndPopup(msgNewsgroups, "addr_newsgroups", newListBoxNode, templateNode); + havePrimaryRecipient = true; + } + if(msgFollowupTo) + awSetInputAndPopup(msgFollowupTo, "addr_followup", newListBoxNode, templateNode); + + // If it's a new message, we need to add an extra empty recipient. + if (!havePrimaryRecipient) + _awSetInputAndPopup("", "addr_to", newListBoxNode, templateNode); + awFitDummyRows(2); + + // CompFields2Recipients is called whenever a user replies or edits an existing message. + // We want to add all of the recipients for this message to the ignore list for spell check + let currentAddress = gCurrentIdentity ? gCurrentIdentity.fullAddress : ""; + addRecipientsToIgnoreList([currentAddress,msgTo,msgCC,msgBCC].filter(adr => adr).join(", ")); + } +} + +function awSetInputAndPopupId(inputElem, popupElem, rowNumber) +{ + popupElem.id = "addressCol1#" + rowNumber; + inputElem.id = "addressCol2#" + rowNumber; + inputElem.setAttribute("aria-labelledby", popupElem.id); +} + +function awSetInputAndPopupValue(inputElem, inputValue, popupElem, popupValue, rowNumber) +{ + inputElem.value = inputValue.trimLeft(); + + popupElem.selectedItem = popupElem.childNodes[0].childNodes[awGetSelectItemIndex(popupValue)]; + + if (rowNumber >= 0) + awSetInputAndPopupId(inputElem, popupElem, rowNumber); + + _awSetAutoComplete(popupElem, inputElem); + + onRecipientsChanged(true); +} + +function _awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode) +{ + top.MAX_RECIPIENTS++; + + var newNode = templateNode.cloneNode(true); + parentNode.appendChild(newNode); // we need to insert the new node before we set the value of the select element! + + var input = newNode.getElementsByTagName(awInputElementName()); + var select = newNode.getElementsByTagName(awSelectElementName()); + + if (input && input.length == 1 && select && select.length == 1) + awSetInputAndPopupValue(input[0], inputValue, select[0], popupValue, top.MAX_RECIPIENTS) +} + +function awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode) +{ + if ( inputValue && popupValue ) + { + var addressArray = inputValue.split(","); + + for ( var index = 0; index < addressArray.length; index++ ) + _awSetInputAndPopup(addressArray[index], popupValue, parentNode, templateNode); + } +} + +function awSetInputAndPopupFromArray(inputArray, popupValue, parentNode, templateNode) +{ + if (popupValue) + { + for (let recipient of inputArray) + _awSetInputAndPopup(recipient, popupValue, parentNode, templateNode); + } +} + +function awRemoveRecipients(msgCompFields, recipientType, recipientsList) +{ + if (!msgCompFields) + return; + + var recipientArray = msgCompFields.splitRecipients(recipientsList, false); + + for (var index = 0; index < recipientArray.length; index++) + for (var row = 1; row <= top.MAX_RECIPIENTS; row ++) + { + var popup = awGetPopupElement(row); + if (popup.value == recipientType) { + var input = awGetInputElement(row); + if (input.value == recipientArray[index]) + { + awSetInputAndPopupValue(input, "", popup, "addr_to", -1); + break; + } + } + } +} + +function awAddRecipients(msgCompFields, recipientType, recipientsList) +{ + if (!msgCompFields) + return; + + var recipientArray = msgCompFields.splitRecipients(recipientsList, false); + + for (var index = 0; index < recipientArray.length; index++) + awAddRecipient(recipientType, recipientArray[index]); +} + +// this was broken out of awAddRecipients so it can be re-used...adds a new row matching recipientType and +// drops in the single address. +function awAddRecipient(recipientType, address) +{ + for (var row = 1; row <= top.MAX_RECIPIENTS; row ++) + { + if (awGetInputElement(row).value == "") + break; + } + + if (row > top.MAX_RECIPIENTS) + awAppendNewRow(false); + + awSetInputAndPopupValue(awGetInputElement(row), address, awGetPopupElement(row), recipientType, row); + + /* be sure we still have an empty row left at the end */ + if (row == top.MAX_RECIPIENTS) + { + awAppendNewRow(true); + awSetInputAndPopupValue(awGetInputElement(top.MAX_RECIPIENTS), "", awGetPopupElement(top.MAX_RECIPIENTS), recipientType, top.MAX_RECIPIENTS); + } + + // add the recipient to our spell check ignore list + addRecipientsToIgnoreList(address); +} + +function awTestRowSequence() +{ + /* + This function is for debug and testing purpose only, normal users should not run it! + + Everytime we insert or delete a row, we must be sure we didn't break the ID sequence of + the addressing widget rows. This function will run a quick test to see if the sequence still ok + + You need to define the pref mail.debug.test_addresses_sequence to true in order to activate it + */ + + var test_sequence; + if (Services.prefs.getPrefType("mail.debug.test_addresses_sequence") == Ci.nsIPrefBranch.PREF_BOOL) + test_sequence = Services.prefs.getBoolPref("mail.debug.test_addresses_sequence"); + if (!test_sequence) + return true; + + /* debug code to verify the sequence still good */ + + var listbox = document.getElementById('addressingWidget'); + var listitems = listbox.getElementsByTagName('listitem'); + if (listitems.length >= top.MAX_RECIPIENTS ) + { + for (var i = 1; i <= listitems.length; i ++) + { + var item = listitems [i - 1]; + let inputID = item.querySelector(awInputElementName()).id.split("#")[1]; + let popupID = item.querySelector(awSelectElementName()).id.split("#")[1]; + if (inputID != i || popupID != i) + { + dump("#ERROR: sequence broken at row " + i + ", inputID=" + inputID + ", popupID=" + popupID + "\n"); + return false; + } + dump("---SEQUENCE OK---\n"); + return true; + } + } + else + dump("#ERROR: listitems.length(" + listitems.length + ") < top.MAX_RECIPIENTS(" + top.MAX_RECIPIENTS + ")\n"); + + return false; +} + +function awCleanupRows() +{ + var maxRecipients = top.MAX_RECIPIENTS; + var rowID = 1; + + for (var row = 1; row <= maxRecipients; row ++) + { + var inputElem = awGetInputElement(row); + if (inputElem.value == "" && row < maxRecipients) + awRemoveRow(awGetRowByInputElement(inputElem)); + else + { + awSetInputAndPopupId(inputElem, awGetPopupElement(row), rowID); + rowID ++; + } + } + + awTestRowSequence(); +} + +function awDeleteRow(rowToDelete) +{ + /* When we delete a row, we must reset the id of others row in order to not break the sequence */ + var maxRecipients = top.MAX_RECIPIENTS; + awRemoveRow(rowToDelete); + + // assume 2 column update (input and popup) + for (var row = rowToDelete + 1; row <= maxRecipients; row ++) + awSetInputAndPopupId(awGetInputElement(row), awGetPopupElement(row), (row-1)); + + awTestRowSequence(); +} + +function awClickEmptySpace(target, setFocus) +{ + if (target == null || + (target.localName != "listboxbody" && + target.localName != "listcell" && + target.localName != "listitem")) + return; + + let lastInput = awGetInputElement(top.MAX_RECIPIENTS); + + if ( lastInput && lastInput.value ) + awAppendNewRow(setFocus); + else if (setFocus) + awSetFocusTo(lastInput); +} + +function awReturnHit(inputElement) +{ + let row = awGetRowByInputElement(inputElement); + let nextInput = awGetInputElement(row+1); + + if ( !nextInput ) + { + if ( inputElement.value ) + awAppendNewRow(true); + else // No address entered, switch to Subject field + { + var subjectField = document.getElementById( 'msgSubject' ); + subjectField.select(); + subjectField.focus(); + } + } + else + { + nextInput.select(); + awSetFocusTo(nextInput); + } + + // be sure to add the recipient to our ignore list + // when the user hits enter in an autocomplete widget... + addRecipientsToIgnoreList(inputElement.value); +} + +function awDeleteHit(inputElement) +{ + let row = awGetRowByInputElement(inputElement); + + /* 1. don't delete the row if it's the last one remaining, just reset it! */ + if (top.MAX_RECIPIENTS <= 1) + { + inputElement.value = ""; + return; + } + + /* 2. Set the focus to the previous field if possible */ + // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal. + if (row > 1) + awSetFocusTo(awGetInputElement(row - 1)) + else + awSetFocusTo(awGetInputElement(2)) + + /* 3. Delete the row */ + awDeleteRow(row); +} + +function awAppendNewRow(setFocus) +{ + var listbox = document.getElementById('addressingWidget'); + var listitem1 = awGetListItem(1); + + if ( listbox && listitem1 ) + { + var lastRecipientType = awGetPopupElement(top.MAX_RECIPIENTS).value; + + var nextDummy = awGetNextDummyRow(); + var newNode = listitem1.cloneNode(true); + if (nextDummy) + listbox.replaceChild(newNode, nextDummy); + else + listbox.appendChild(newNode); + + top.MAX_RECIPIENTS++; + + var input = newNode.getElementsByTagName(awInputElementName()); + if ( input && input.length == 1 ) + { + input[0].value = ""; + + // We always clone the first row. The problem is that the first row + // could be focused. When we clone that row, we end up with a cloned + // XUL textbox that has a focused attribute set. Therefore we think + // we're focused and don't properly refocus. The best solution to this + // would be to clone a template row that didn't really have any presentation, + // rather than using the real visible first row of the listbox. + // + // For now we'll just put in a hack that ensures the focused attribute + // is never copied when the node is cloned. + if (input[0].getAttribute('focused') != '') + input[0].removeAttribute('focused'); + } + var select = newNode.getElementsByTagName(awSelectElementName()); + if ( select && select.length == 1 ) + { + // It only makes sense to clone some field types; others + // should not be cloned, since it just makes the user have + // to go to the trouble of selecting something else. In such + // cases let's default to 'To' (a reasonable default since + // we already default to 'To' on the first dummy field of + // a new message). + switch (lastRecipientType) + { + case "addr_reply": + case "addr_other": + select[0].selectedIndex = awGetSelectItemIndex("addr_to"); + break; + case "addr_followup": + select[0].selectedIndex = awGetSelectItemIndex("addr_newsgroups"); + break; + default: + // e.g. "addr_to","addr_cc","addr_bcc","addr_newsgroups": + select[0].selectedIndex = awGetSelectItemIndex(lastRecipientType); + } + + awSetInputAndPopupId(input[0], select[0], top.MAX_RECIPIENTS); + + if (input) + _awSetAutoComplete(select[0], input[0]); + } + + // Focus the new input widget. + if (setFocus && input[0] ) + awSetFocusTo(input[0]); + } +} + +// functions for accessing the elements in the addressing widget + +/** + * Returns the recipient type popup for a row. + * + * @param row Index of the recipient row to return. Starts at 1. + * @return This returns the menulist (not its child menupopup), despite the + * function name. + */ +function awGetPopupElement(row) +{ + return document.getElementById("addressCol1#" + row); +} + +/** + * Returns the recipient inputbox for a row. + * + * @param row Index of the recipient row to return. Starts at 1. + * @return This returns the textbox element. + */ +function awGetInputElement(row) +{ + return document.getElementById("addressCol2#" + row); +} + +function awGetElementByCol(row, col) +{ + var colID = "addressCol" + col + "#" + row; + return document.getElementById(colID); +} + +function awGetListItem(row) +{ + var listbox = document.getElementById('addressingWidget'); + + if ( listbox && row > 0) + { + var listitems = listbox.getElementsByTagName('listitem'); + if ( listitems && listitems.length >= row ) + return listitems[row-1]; + } + return 0; +} + +function awGetRowByInputElement(inputElement) +{ + var row = 0; + if (inputElement) { + var listitem = inputElement.parentNode.parentNode; + while (listitem) { + if (listitem.localName == "listitem") + ++row; + listitem = listitem.previousSibling; + } + } + return row; +} + + +// Copy Node - copy this node and insert ahead of the (before) node. Append to end if before=0 +function awCopyNode(node, parentNode, beforeNode) +{ + var newNode = node.cloneNode(true); + + if ( beforeNode ) + parentNode.insertBefore(newNode, beforeNode); + else + parentNode.appendChild(newNode); + + return newNode; +} + +// remove row + +function awRemoveRow(row) +{ + awGetListItem(row).remove(); + awFitDummyRows(); + + top.MAX_RECIPIENTS --; +} + +/** + * Set focus to the specified element, typically a recipient input element. + * We do this asynchronusly to allow other processes like adding or removing rows + * to complete before shifting focus. + * + * @param element the element to receive focus asynchronously + */ +function awSetFocusTo(element) { + // Remember the (input) element to focus for asynchronous focusing, so that we + // play safe if this gets called again and the original element gets removed + // before we can focus it. + top.awInputToFocus = element; + setTimeout(_awSetFocusTo, 0); +} + +function _awSetFocusTo() { + top.awInputToFocus.focus(); +} + +// Deprecated - use awSetFocusTo() instead. +// ### TODO: This function should be removed if we're sure addons aren't using it. +function awSetFocus(row, inputElement) { + awSetFocusTo(inputElement); +} + +function awTabFromRecipient(element, event) { + var row = awGetRowByInputElement(element); + if (!event.shiftKey && row < top.MAX_RECIPIENTS) { + var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based. + var listBox = document.getElementById("addressingWidget"); + listBox.listBoxObject.ensureIndexIsVisible(listBoxRow + 1); + } + + // be sure to add the recipient to our ignore list + // when the user tabs out of an autocomplete line... + addRecipientsToIgnoreList(element.value); +} + +function awTabFromMenulist(element, event) +{ + var row = awGetRowByInputElement(element); + if (event.shiftKey && row > 1) { + var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based. + var listBox = document.getElementById("addressingWidget"); + listBox.listBoxObject.ensureIndexIsVisible(listBoxRow - 1); + } +} + +function awGetNumberOfRecipients() +{ + return top.MAX_RECIPIENTS; +} + +function DropOnAddressingTarget(event, onWidget) { + let dragSession = gDragService.getCurrentSession(); + + let trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor("text/x-moz-address"); + + let added = false; + for (let i = 0; i < dragSession.numDropItems; ++i) { + dragSession.getData(trans, i); + let dataObj = {}; + let bestFlavor = {}; + let len = {}; + + // Ensure we catch any empty data that may have slipped through. + try { + trans.getAnyTransferData(bestFlavor, dataObj, len); + } catch(ex) { + continue; + } + if (dataObj) { + dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString); + } + if (!dataObj) { + continue; + } + + // Pull the address out of the data object. + let address = dataObj.data.substring(0, len.value); + if (!address) { + continue; + } + + if (onWidget) { + // Break down and add each address. + parseAndAddAddresses(address, + awGetPopupElement(top.MAX_RECIPIENTS).value); + } else { + // Add address into the bucket. + DropRecipient(address); + } + added = true; + } + + // We added at least one address during the drop. + // Disable the default handler and stop propagating the event + // to avoid data being dropped twice. + if (added) { + event.preventDefault(); + event.stopPropagation(); + } +} + +function _awSetAutoComplete(selectElem, inputElem) +{ + let params = JSON.parse(inputElem.getAttribute('autocompletesearchparam')); + params.type = selectElem.value; + inputElem.setAttribute('autocompletesearchparam', JSON.stringify(params)); +} + +function awSetAutoComplete(rowNumber) +{ + var inputElem = awGetInputElement(rowNumber); + var selectElem = awGetPopupElement(rowNumber); + _awSetAutoComplete(selectElem, inputElem) +} + +function awRecipientTextCommand(userAction, element) +{ + if (userAction == "typing" || userAction == "scrolling") + awReturnHit(element); +} + +// Called when an autocomplete session item is selected and the status of +// the session it was selected from is nsIAutoCompleteStatus::failureItems. +// +// As of this writing, the only way that can happen is when an LDAP +// autocomplete session returns an error to be displayed to the user. +// +// There are hardcoded messages in here, but these are just fallbacks for +// when string bundles have already failed us. +// +function awRecipientErrorCommand(errItem, element) +{ + // remove the angle brackets from the general error message to construct + // the title for the alert. someday we'll pass this info using a real + // exception object, and then this code can go away. + // + var generalErrString; + if (errItem.value != "") { + generalErrString = errItem.value.slice(1, errItem.value.length-1); + } else { + generalErrString = "Unknown LDAP server problem encountered"; + } + + // try and get the string of the specific error to contruct the complete + // err msg, otherwise fall back to something generic. This message is + // handed to us as an nsISupportsString in the param slot of the + // autocomplete error item, by agreement documented in + // nsILDAPAutoCompFormatter.idl + // + var specificErrString = ""; + try { + var specificError = errItem.param.QueryInterface(Ci.nsISupportsString); + specificErrString = specificError.data; + } catch (ex) { + } + if (specificErrString == "") { + specificErrString = "Internal error"; + } + + Services.prompt.alert(window, generalErrString, specificErrString); +} + +function awRecipientKeyPress(event, element) +{ + switch(event.key) { + case "ArrowUp": + awArrowHit(element, -1); + break; + case "ArrowDown": + awArrowHit(element, 1); + break; + case "Enter": + case "Tab": + // if the user text contains a comma or a line return, ignore + if (element.value.includes(',')) { + var addresses = element.value; + element.value = ""; // clear out the current line so we don't try to autocomplete it.. + parseAndAddAddresses(addresses, awGetPopupElement(awGetRowByInputElement(element)).value); + } + else if (event.key == "Tab") + awTabFromRecipient(element, event); + + break; + } +} + +function awArrowHit(inputElement, direction) +{ + var row = awGetRowByInputElement(inputElement) + direction; + if (row) { + var nextInput = awGetInputElement(row); + + if (nextInput) + awSetFocusTo(nextInput); + else if (inputElement.value) + awAppendNewRow(true); + } +} + +function awRecipientKeyDown(event, element) +{ + switch(event.key) { + case "Delete": + case "Backspace": + /* do not query directly the value of the text field else the autocomplete widget could potentially + alter it value while doing some internal cleanup, instead, query the value through the first child + */ + if (!element.value) + awDeleteHit(element); + + //We need to stop the event else the listbox will receive it and the function + //awKeyDown will be executed! + event.stopPropagation(); + break; + } +} + +function awKeyDown(event, listboxElement) +{ + switch(event.key) { + case "Delete": + case "Backspace": + /* Warning, the listboxElement.selectedItems will change everytime we delete a row */ + var length = listboxElement.selectedCount; + for (var i = 1; i <= length; i++) { + var inputs = listboxElement.selectedItem.getElementsByTagName(awInputElementName()); + if (inputs && inputs.length == 1) + awDeleteHit(inputs[0]); + } + break; + } +} + +function awMenulistKeyPress(event, element) +{ + switch(event.key) { + case "Tab": + awTabFromMenulist(element, event); + break; + } +} + +/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */ + +var gAWContentHeight = 0; +var gAWRowHeight = 0; + +function awFitDummyRows() +{ + awCalcContentHeight(); + awCreateOrRemoveDummyRows(); +} + +function awCreateOrRemoveDummyRows() +{ + var listbox = document.getElementById("addressingWidget"); + var listboxHeight = listbox.boxObject.height; + + // remove rows to remove scrollbar + let kids = listbox.querySelectorAll('[_isDummyRow]'); + for (let i = kids.length - 1; gAWContentHeight > listboxHeight && i >= 0; --i) { + gAWContentHeight -= gAWRowHeight; + kids[i].remove(); + } + + // add rows to fill space + if (gAWRowHeight) { + while (gAWContentHeight + gAWRowHeight < listboxHeight) { + awCreateDummyItem(listbox); + gAWContentHeight += gAWRowHeight; + } + } +} + +function awCalcContentHeight() +{ + var listbox = document.getElementById("addressingWidget"); + var items = listbox.getElementsByTagName("listitem"); + + gAWContentHeight = 0; + if (items.length > 0) { + // all rows are forced to a uniform height in xul listboxes, so + // find the first listitem with a boxObject and use it as precedent + var i = 0; + do { + gAWRowHeight = items[i].boxObject.height; + ++i; + } while (i < items.length && !gAWRowHeight); + gAWContentHeight = gAWRowHeight*items.length; + } +} + +function awCreateDummyItem(aParent) +{ + var titem = document.createElement("listitem"); + titem.setAttribute("_isDummyRow", "true"); + titem.setAttribute("class", "dummy-row"); + + for (var i = awGetNumberOfCols(); i > 0; i--) + awCreateDummyCell(titem); + + if (aParent) + aParent.appendChild(titem); + + return titem; +} + +function awCreateDummyCell(aParent) +{ + var cell = document.createElement("listcell"); + cell.setAttribute("class", "addressingWidgetCell dummy-row-cell"); + if (aParent) + aParent.appendChild(cell); + + return cell; +} + +function awGetNextDummyRow() +{ + // gets the next row from the top down + return document.querySelector('#addressingWidget > [_isDummyRow]'); +} + +function awSizerListen() +{ + // when splitter is clicked, fill in necessary dummy rows each time the mouse is moved + awCalcContentHeight(); // precalculate + document.addEventListener("mousemove", awSizerMouseMove, true); + document.addEventListener("mouseup", awSizerMouseUp); +} + +function awSizerMouseMove() +{ + awCreateOrRemoveDummyRows(2); +} + +function awSizerMouseUp() +{ + document.removeEventListener("mousemove", awSizerMouseMove); + document.removeEventListener("mouseup", awSizerMouseUp); +} + +function awSizerResized(aSplitter) +{ + // set the height on the listbox rather than on the toolbox + var listbox = document.getElementById("addressingWidget"); + listbox.height = listbox.boxObject.height; + // remove all the heights set on the splitter's previous siblings + for (let sib = aSplitter.previousSibling; sib; sib = sib.previousSibling) + sib.removeAttribute("height"); +} + +function awDocumentKeyPress(event) +{ + try { + var id = event.target.id; + if (id.startsWith('addressCol1')) + awRecipientKeyPress(event, event.target); + } catch (e) { } +} + +function awRecipientInputCommand(event, inputElement) +{ + gContentChanged=true; + setupAutocomplete(); +} + +// Given an arbitrary block of text like a comma delimited list of names or a names separated by spaces, +// we will try to autocomplete each of the names and then take the FIRST match for each name, adding it the +// addressing widget on the compose window. + +var gAutomatedAutoCompleteListener = null; + +function parseAndAddAddresses(addressText, recipientType) +{ + // strip any leading >> characters inserted by the autocomplete widget + var strippedAddresses = addressText.replace(/.* >> /, ""); + + var addresses = MailServices.headerParser + .makeFromDisplayAddress(strippedAddresses); + + if (addresses.length) + { + // we need to set up our own autocomplete session and search for results + + setupAutocomplete(); // be safe, make sure we are setup + if (!gAutomatedAutoCompleteListener) + gAutomatedAutoCompleteListener = new AutomatedAutoCompleteHandler(); + + gAutomatedAutoCompleteListener.init(addresses.map(addr => addr.toString()), + addresses.length, recipientType); + } +} + +function AutomatedAutoCompleteHandler() +{ +} + +// state driven self contained object which will autocomplete a block of addresses without any UI. +// force picks the first match and adds it to the addressing widget, then goes on to the next +// name to complete. + +AutomatedAutoCompleteHandler.prototype = +{ + param: this, + sessionName: null, + namesToComplete: {}, + numNamesToComplete: 0, + indexIntoNames: 0, + + numSessionsToSearch: 0, + numSessionsSearched: 0, + recipientType: null, + searchResults: null, + + init:function(namesToComplete, numNamesToComplete, recipientType) + { + this.indexIntoNames = 0; + this.numNamesToComplete = numNamesToComplete; + this.namesToComplete = namesToComplete; + + this.recipientType = recipientType; + + // set up the auto complete sessions to use + setupAutocomplete(); + this.autoCompleteNextAddress(); + }, + + autoCompleteNextAddress:function() + { + this.numSessionsToSearch = 0; + this.numSessionsSearched = 0; + this.searchResults = new Array; + + if (this.indexIntoNames < this.numNamesToComplete && this.namesToComplete[this.indexIntoNames]) + { + /* XXX This is used to work, until switching to the new toolkit broke it + We should fix it see bug 456550. + if (!this.namesToComplete[this.indexIntoNames].includes('@')) // don't autocomplete if address has an @ sign in it + { + // make sure total session count is updated before we kick off ANY actual searches + if (gAutocompleteSession) + this.numSessionsToSearch++; + + if (gLDAPSession && gCurrentAutocompleteDirectory) + this.numSessionsToSearch++; + + if (gAutocompleteSession) + { + gAutocompleteSession.onAutoComplete(this.namesToComplete[this.indexIntoNames], null, this); + // AB searches are actually synchronous. So by the time we get here we have already looked up results. + + // if we WERE going to also do an LDAP lookup, then check to see if we have a valid match in the AB, if we do + // don't bother with the LDAP search too just return + + if (gLDAPSession && gCurrentAutocompleteDirectory && this.searchResults[0] && this.searchResults[0].defaultItemIndex != -1) + { + this.processAllResults(); + return; + } + } + + if (gLDAPSession && gCurrentAutocompleteDirectory) + gLDAPSession.onStartLookup(this.namesToComplete[this.indexIntoNames], null, this); + } + */ + + if (!this.numSessionsToSearch) + this.processAllResults(); // ldap and ab are turned off, so leave text alone + } + }, + + onStatus:function(aStatus) + { + return; + }, + + onAutoComplete: function(aResults, aStatus) + { + // store the results until all sessions are done and have reported in + if (aResults) + this.searchResults[this.numSessionsSearched] = aResults; + + this.numSessionsSearched++; // bump our counter + + if (this.numSessionsToSearch <= this.numSessionsSearched) + setTimeout('gAutomatedAutoCompleteListener.processAllResults()', 0); // we are all done + }, + + processAllResults: function() + { + // Take the first result and add it to the compose window + var addressToAdd; + + // loop through the results looking for the non default case (default case is the address book with only one match, the default domain) + var sessionIndex; + + var searchResultsForSession; + + for (sessionIndex in this.searchResults) + { + searchResultsForSession = this.searchResults[sessionIndex]; + if (searchResultsForSession && searchResultsForSession.defaultItemIndex > -1) + { + addressToAdd = searchResultsForSession.items + .queryElementAt(searchResultsForSession.defaultItemIndex, + Ci.nsIAutoCompleteItem).value; + break; + } + } + + // still no match? loop through looking for the -1 default index + if (!addressToAdd) + { + for (sessionIndex in this.searchResults) + { + searchResultsForSession = this.searchResults[sessionIndex]; + if (searchResultsForSession && searchResultsForSession.defaultItemIndex == -1) + { + addressToAdd = searchResultsForSession.items + .queryElementAt(0, Ci.nsIAutoCompleteItem).value; + break; + } + } + } + + // no matches anywhere...just use what we were given + if (!addressToAdd) + addressToAdd = this.namesToComplete[this.indexIntoNames]; + + // that will automatically set the focus on a new available row, and make sure it is visible + awAddRecipient(this.recipientType ? this.recipientType : "addr_to", addressToAdd); + + this.indexIntoNames++; + this.autoCompleteNextAddress(); + }, + + QueryInterface : function(iid) + { + if (iid.equals(Ci.nsIAutoCompleteListener) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + } +} diff --git a/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul b/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul new file mode 100644 index 0000000000..efd1c6007a --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<overlay id="mailComposeOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <menupopup id="menu_EditPopup" onpopupshowing="updateEditItems();"> + <menuitem id="menu_inlineSpellCheck" + oncommand="EnableInlineSpellCheck(!InlineSpellCheckerUI.enabled);"/> + <menuitem id="menu_accountmgr" + insertafter="sep_preferences" + command="cmd_account"/> + </menupopup> +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/messengercompose.xul b/comm/suite/mailnews/components/compose/content/messengercompose.xul new file mode 100644 index 0000000000..89126d814d --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/messengercompose.xul @@ -0,0 +1,720 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messengercompose/messengercompose.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/editorFormatToolbar.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/addressingWidget.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/smime/msgCompSMIMEOverlay.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/content/bindings.css" type="text/css"?> + +<?xul-overlay href="chrome://communicator/content/charsetOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/sidebar/sidebarOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/messengercompose/msgComposeContextOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> +<?xul-overlay href="chrome://editor/content/editorOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/messengercompose/mailComposeOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/mailOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % messengercomposeDTD SYSTEM "chrome://messenger/locale/messengercompose/messengercompose.dtd" > +%messengercomposeDTD; +<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" > +%messengerDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % utilityDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd"> +%utilityDTD; +<!ENTITY % msgCompSMIMEDTD SYSTEM "chrome://messenger-smime/locale/msgCompSMIMEOverlay.dtd"> +%msgCompSMIMEDTD; +]> + +<window id="msgcomposeWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:nc="http://home.netscape.com/NC-rdf#" + onunload="ComposeUnload()" + onload="ComposeLoad()" + onclose="return DoCommandClose()" + onfocus="EditorOnFocus()" + title="&msgComposeWindow.title;" + toggletoolbar="true" + lightweightthemes="true" + lightweightthemesfooter="status-bar" + windowtype="msgcompose" + macanimationtype="document" + drawtitle="true" + width="640" height="480" + persist="screenX screenY width height sizemode"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_composeMsgs" src="chrome://messenger/locale/messengercompose/composeMsgs.properties"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + <stringbundle id="bundle_offlinePrompts" src="chrome://messenger/locale/offline.properties"/> + <stringbundle id="languageBundle" src="chrome://global/locale/languageNames.properties"/> + <stringbundle id="brandBundle" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundle_comp_smime" src="chrome://messenger-smime/locale/msgCompSMIMEOverlay.properties"/> + </stringbundleset> + + <script src="chrome://communicator/content/contentAreaClick.js"/> + <script src="chrome://global/content/printUtils.js"/> + <script src="chrome://messenger/content/accountUtils.js"/> + <script src="chrome://messenger/content/mail-offline.js"/> + <script src="chrome://editor/content/editor.js"/> + <script src="chrome://messenger/content/messengercompose/MsgComposeCommands.js"/> + <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/> + <script src="chrome://messenger/content/addressbook/abDragDrop.js"/> + <script src="chrome://messenger-smime/content/msgCompSMIMEOverlay.js"/> + + <commandset id="composeCommands"> + <commandset id="msgComposeCommandUpdate" + commandupdater="true" + events="focus" + oncommandupdate="CommandUpdate_MsgCompose()"/> + + <commandset id="editorCommands"/> + <commandset id="commonEditorMenuItems"/> + <commandset id="composerMenuItems"/> + <commandset id="composerEditMenuItems"/> + <commandset id="composerStyleMenuItems"/> + <commandset id="composerTableMenuItems"/> + <commandset id="composerListMenuItems"/> + <commandset id="tasksCommands"/> + <!-- File Menu --> + <command id="cmd_attachFile" oncommand="goDoCommand('cmd_attachFile')"/> + <command id="cmd_attachPage" oncommand="goDoCommand('cmd_attachPage')"/> + <command id="cmd_attachVCard" checked="false" oncommand="ToggleAttachVCard(event.target)"/> + <command id="cmd_save" oncommand="goDoCommand('cmd_save')"/> + <command id="cmd_saveAsFile" oncommand="goDoCommand('cmd_saveAsFile')"/> + <command id="cmd_saveAsDraft" oncommand="goDoCommand('cmd_saveAsDraft')"/> + <command id="cmd_saveAsTemplate" oncommand="goDoCommand('cmd_saveAsTemplate')"/> + <command id="cmd_sendButton" oncommand="goDoCommand('cmd_sendButton')"/> + <command id="cmd_sendNow" oncommand="goDoCommand('cmd_sendNow')"/> + <command id="cmd_sendWithCheck" oncommand="goDoCommand('cmd_sendWithCheck')"/> + <command id="cmd_sendLater" oncommand="goDoCommand('cmd_sendLater')"/> + + <!-- Edit Menu --> + <!--command id="cmd_pasteQuote"/ DO NOT INCLUDE THOSE COMMANDS ELSE THE EDIT MENU WILL BE BROKEN! --> + <!--command id="cmd_find"/--> + <!--command id="cmd_findNext"/--> + <!--command id="cmd_findReplace"/--> + <command id="cmd_renameAttachment" oncommand="goDoCommand('cmd_renameAttachment')" disabled="true"/> + <command id="cmd_openAttachment" oncommand="goDoCommand('cmd_openAttachment')"/> + <command id="cmd_account" + label="&accountManagerCmd.label;" + accesskey="&accountManagerCmd.accesskey;" + oncommand="goDoCommand('cmd_account');"/> + + <!-- Options Menu --> + <command id="cmd_selectAddress" oncommand="goDoCommand('cmd_selectAddress')"/> + <command id="cmd_outputFormat" oncommand="OutputFormatMenuSelect(event.target)"/> + <command id="cmd_quoteMessage" oncommand="goDoCommand('cmd_quoteMessage')"/> + <command id="cmd_viewSecurityStatus" oncommand="showMessageComposeSecurityStatus();"/> + </commandset> + + <broadcasterset id="composeBroadcasters"> + <broadcaster id="Communicator:WorkMode"/> + <broadcaster id="securityStatus" crypto="" signing=""/> + </broadcasterset> + + <observes element="securityStatus" attribute="crypto"/> + <observes element="securityStatus" attribute="signing"/> + + <broadcasterset id="mainBroadcasterSet"/> + + <keyset id="tasksKeys"> + <!-- File Menu --> + <key id="key_send" keycode="&sendCmd.keycode;" observes="cmd_sendWithCheck" modifiers="accel"/> + <key id="key_sendLater" keycode="&sendLaterCmd.keycode;" observes="cmd_sendLater" modifiers="accel, shift"/> + + <!-- Options Menu --> + <!-- key id="key_selectAddresses" key="&selectAddressCmd.key;" command="cmd_selectAddress"/ --> + + <key id="showHideSidebar"/> + <!-- Tab/F6 Keys --> + <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control"/> + <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="shift"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);"/> + <key keycode="VK_ESCAPE" oncommand="handleEsc();"/> + </keyset> + <keyset id="editorKeys"/> + <keyset id="composeKeys"> +#ifndef XP_MACOSX + <key id="key_renameAttachment" keycode="VK_F2" + oncommand="goDoCommand('cmd_renameAttachment');"/> +#endif + </keyset> + + <popupset id="contentAreaContextSet"/> + + <popupset id="editorPopupSet"> + <menupopup id="sidebarPopup"/> + + <menupopup id="msgComposeAttachmentContext" + onpopupshowing="updateEditItems();"> + <menuitem label="&openAttachment.label;" + accesskey="&openAttachment.accesskey;" + command="cmd_openAttachment"/> + <menuitem accesskey="&deleteAttachment.accesskey;" + command="cmd_delete"/> + <menuitem label="&renameAttachment.label;" + accesskey="&renameAttachment.accesskey;" + command="cmd_renameAttachment"/> + <menuitem label="&selectAllCmd.label;" + accesskey="&selectAllAttachments.accesskey;" + command="cmd_selectAll"/> + <menuseparator/> + <menuitem label="&attachFile.label;" + accesskey="&attachFile.accesskey;" + command="cmd_attachFile"/> + <menuitem label="&attachPage.label;" + accesskey="&attachPage.accesskey;" + command="cmd_attachPage"/> + </menupopup> + </popupset> + + <menupopup id="blockedContentOptions" value="" + onpopupshowing="onBlockedContentOptionsShowing(event);"> + </menupopup> + + <vbox id="titlebar"/> + + <toolbox id="compose-toolbox" + class="toolbox-top" + mode="full" + defaultmode="full"> + <toolbar id="compose-toolbar-menubar2" + type="menubar" + class="chromeclass-menubar" + persist="collapsed" + grippytooltiptext="&menuBar.tooltip;" + customizable="true" + defaultset="menubar-items" + mode="icons" + iconsize="small" + defaultmode="icons" + defaulticonsize="small" + context="toolbar-context-menu"> + <toolbaritem id="menubar-items" + class="menubar-items" + align="center"> + <menubar id="mail-menubar"> + <menu id="menu_File"> + <menupopup id="menu_FilePopup"> + <menu id="menu_New"> + <menupopup id="menu_NewPopup"> + <menuitem id="menu_newMessage"/> + <menuseparator id="menuNewPopupSeparator"/> + <menuitem id="menu_newCard"/> + <menuitem id="menu_newNavigator"/> + <menuitem id="menu_newPrivateWindow"/> + <menuitem id="menu_newEditor"/> + </menupopup> + </menu> + <menu id="menu_Attach" + label="&attachMenu.label;" + accesskey="&attachMenu.accesskey;"> + <menupopup id="menu_AttachPopup"> + <menuitem id="menu_AttachFile" + label="&attachFileCmd.label;" + accesskey="&attachFileCmd.accesskey;" + command="cmd_attachFile"/> + <menuitem id="menu_AttachPage" + label="&attachPageCmd.label;" + accesskey="&attachPageCmd.accesskey;" + command="cmd_attachPage"/> + <menuseparator id="menuAttachPageSeparator"/> + <menuitem id="menu_AttachPageVCard" + type="checkbox" + label="&attachVCardCmd.label;" + accesskey="&attachVCardCmd.accesskey;" + command="cmd_attachVCard"/> + </menupopup> + </menu> + <menuitem id="menu_close"/> + <menuseparator id="menuFileAfterCloseSeparator"/> + <menuitem id="menu_SaveCmd" + label="&saveCmd.label;" + accesskey="&saveCmd.accesskey;" + key="key_save" + command="cmd_save"/> + <menu id="menu_SaveAsCmd" + label="&saveAsCmd.label;" + accesskey="&saveAsCmd.accesskey;"> + <menupopup id="menu_SaveAsCmdPopup"> + <menuitem id="menu_SaveAsFileCmd" + label="&saveAsFileCmd.label;" + accesskey="&saveAsFileCmd.accesskey;" + command="cmd_saveAsFile"/> + <menuseparator id="menuSaveAfterSaveAsSeparator"/> + <menuitem id="menu_SaveAsDraftCmd" + label="&saveAsDraftCmd.label;" + accesskey="&saveAsDraftCmd.accesskey;" + command="cmd_saveAsDraft"/> + <menuitem id="menu_SaveAsTemplateCmd" + label="&saveAsTemplateCmd.label;" + accesskey="&saveAsTemplateCmd.accesskey;" + command="cmd_saveAsTemplate"/> + </menupopup> + </menu> + <menuseparator id="menuFileAfterSaveAsSeparator"/> + <menuitem id="menu_sendNow" + label="&sendNowCmd.label;" + accesskey="&sendNowCmd.accesskey;" + key="key_send" command="cmd_sendNow"/> + <menuitem id="menu_sendLater" + label="&sendLaterCmd.label;" + accesskey="&sendLaterCmd.accesskey;" + key="key_sendLater" + command="cmd_sendLater"/> + <menuseparator id="menuFileAfterSendLaterSeparator"/> + <menuitem id="menu_printSetup"/> + <menuitem id="menu_printPreview"/> + <menuitem id="menu_print"/> + </menupopup> + </menu> + <menu id="menu_Edit"/> + <menu id="menu_View"> + <menupopup id="menu_View_Popup"> + <menu id="menu_Toolbars"> + <menupopup id="view_toolbars_popup" + onpopupshowing="onViewToolbarsPopupShowing(event)" + oncommand="onViewToolbarCommand(event);"> + <menuitem id="menu_showTaskbar"/> + </menupopup> + </menu> + <menuseparator id="viewMenuBeforeSecurityStatusSeparator"/> + <menuitem id="menu_viewSecurityStatus" + label="&menu_viewSecurityStatus.label;" + accesskey="&menu_viewSecurityStatus.accesskey;" + command="cmd_viewSecurityStatus"/> + </menupopup> + </menu> + + <menu id="insertMenu" + command="cmd_renderedHTMLEnabler"/> + + <menu id="formatMenu" + label="&formatMenu.label;" + accesskey="&formatMenu.accesskey;" + command="cmd_renderedHTMLEnabler"> + <menupopup id="formatMenuPopup"> + <menu id="tableMenu"/> + <menuseparator id="menuFormatAfterTableSeparator"/> + <menuitem id="objectProperties"/> + <menuitem id="colorsAndBackground"/> + </menupopup> + </menu> + + <menu id="optionsMenu" + label="&optionsMenu.label;" + accesskey="&optionsMenu.accesskey;"> + <menupopup id="optionsMenuPopup" + onpopupshowing="setSecuritySettings(1);"> + <menuitem id="menu_selectAddress" + label="&selectAddressCmd.label;" + accesskey="&selectAddressCmd.accesskey;" + command="cmd_selectAddress"/> + <menuitem id="menu_quoteMessage" + label=""eCmd.label;" + accesskey=""eCmd.accesskey;" + command="cmd_quoteMessage"/> + <menuseparator id="menuOptionsAfterQuoteSeparator"/> + <menuitem id="returnReceiptMenu" + type="checkbox" + label="&returnReceiptMenu.label;" + accesskey="&returnReceiptMenu.accesskey;" + checked="false" + oncommand="ToggleReturnReceipt(event.target)"/> + <menuitem id="dsnMenu" + type="checkbox" + label="&dsnMenu.label;" + accesskey="&dsnMenu.accesskey;" + checked="false" + oncommand="ToggleDSN(event.target);"/> + <menu id="outputFormatMenu" + label="&outputFormatMenu.label;" + accesskey="&outputFormatMenu.accesskey;" + command="cmd_outputFormat"> + <menupopup id="outputFormatMenuPopup"> + <menuitem id="format_auto" type="radio" name="output_format" label="&autoFormatCmd.label;" accesskey="&autoFormatCmd.accesskey;" checked="true"/> + <menuitem id="format_plain" type="radio" name="output_format" label="&plainTextFormatCmd.label;" accesskey="&plainTextFormatCmd.accesskey;"/> + <menuitem id="format_html" type="radio" name="output_format" label="&htmlFormatCmd.label;" accesskey="&htmlFormatCmd.accesskey;"/> + <menuitem id="format_both" type="radio" name="output_format" label="&bothFormatCmd.label;" accesskey="&bothFormatCmd.accesskey;"/> + </menupopup> + </menu> + <menu id="priorityMenu" + label="&priorityMenu.label;" + accesskey="&priorityMenu.accesskey;" + oncommand="PriorityMenuSelect(event.target);"> + <menupopup id="priorityMenuPopup" + onpopupshowing="updatePriorityMenu(this);"> + <menuitem id="priority_highest" type="radio" name="priority" label="&highestPriorityCmd.label;" accesskey="&highestPriorityCmd.accesskey;" value="Highest"/> + <menuitem id="priority_high" type="radio" name="priority" label="&highPriorityCmd.label;" accesskey="&highPriorityCmd.accesskey;" value="High"/> + <menuitem id="priority_normal" type="radio" name="priority" label="&normalPriorityCmd.label;" accesskey="&normalPriorityCmd.accesskey;" value="Normal"/> + <menuitem id="priority_low" type="radio" name="priority" label="&lowPriorityCmd.label;" accesskey="&lowPriorityCmd.accesskey;" value="Low"/> + <menuitem id="priority_lowest" type="radio" name="priority" label="&lowestPriorityCmd.label;" accesskey="&lowestPriorityCmd.accesskey;" value="Lowest"/> + </menupopup> + </menu> + <menu id="charsetMenu" + onpopupshowing="UpdateCharsetMenu(gMsgCompose.compFields.characterSet, this);" + oncommand="ComposeSetCharacterSet(event);"> + <menupopup id="charsetPopup" detectors="false"/> + </menu> + <menu id="fccMenu" + label="&fileCarbonCopyCmd.label;" + accesskey="&fileCarbonCopyCmd.accesskey;" + oncommand="MessageFcc(event.target._folder);"> + <menupopup id="fccMenuPopup" + type="folder" + mode="filing" + showFileHereLabel="true" + fileHereLabel="&fileHereMenu.label;"/> + </menu> + <menuseparator id="smimeOptionsSeparator"/> + <menuitem id="menu_securityEncryptRequire1" + type="checkbox" + label="&menu_securityEncryptRequire.label;" + accesskey="&menu_securityEncryptRequire.accesskey;" + oncommand="toggleEncryptMessage();"/> + <menuitem id="menu_securitySign1" + type="checkbox" + label="&menu_securitySign.label;" + accesskey="&menu_securitySign.accesskey;" + oncommand="toggleSignMessage();"/> + </menupopup> + </menu> + <menu id="tasksMenu"/> + <menu id="windowMenu"/> + <menu id="menu_Help"/> + </menubar> + </toolbaritem> + </toolbar> + + <toolbar id="composeToolbar" + class="toolbar-primary chromeclass-toolbar" + persist="collapsed" + grippytooltiptext="&mailToolbar.tooltip;" + toolbarname="&showComposeToolbarCmd.label;" + accesskey="&showComposeToolbarCmd.accesskey;" + customizable="true" + defaultset="button-send,separator,button-address,button-attach,spellingButton,button-security,separator,button-save,spring,throbber-box" + context="toolbar-context-menu"> + <toolbarbutton id="button-send" + class="toolbarbutton-1" + label="&sendButton.label;" + tooltiptext="&sendButton.tooltip;" + now_label="&sendButton.label;" + now_tooltiptext="&sendButton.tooltip;" + later_label="&sendLaterCmd.label;" + later_tooltiptext="&sendlaterButton.tooltip;" + removable="true" + command="cmd_sendButton"> + <observes element="Communicator:WorkMode" + attribute="offline"/> + </toolbarbutton> + + <toolbarbutton id="button-address" + class="toolbarbutton-1" + label="&addressButton.label;" + tooltiptext="&addressButton.tooltip;" + removable="true" + command="cmd_selectAddress"/> + + <toolbarbutton id="button-attach" + type="menu-button" + class="toolbarbutton-1" + label="&attachButton.label;" + tooltiptext="&attachButton.tooltip;" + removable="true" + command="cmd_attachFile"> + <menupopup id="button-attachPopup"> + <menuitem id="button-attachFile" + label="&attachFileCmd.label;" + accesskey="&attachFileCmd.accesskey;" + command="cmd_attachFile"/> + <menuitem id="button-attachPage" + label="&attachPageCmd.label;" + accesskey="&attachPageCmd.accesskey;" + command="cmd_attachPage"/> + <menuseparator id="buttonAttachAfterPageSeparator"/> + <menuitem id="button-attachVCard" + type="checkbox" + label="&attachVCardCmd.label;" + accesskey="&attachVCardCmd.accesskey;" + command="cmd_attachVCard"/> + </menupopup> + </toolbarbutton> + + <toolbarbutton id="spellingButton" + type="menu-button" + class="toolbarbutton-1" + label="&spellingButton.label;" + removable="true" + command="cmd_spelling"> + <!-- this popup gets dynamically generated --> + <menupopup id="languageMenuList" + oncommand="ChangeLanguage(event);" + onpopupshowing="OnShowDictionaryMenu(event.target);"/> + </toolbarbutton> + + <toolbarbutton id="button-save" + type="menu-button" + class="toolbarbutton-1" + label="&saveButton.label;" + tooltiptext="&saveButton.tooltip;" + removable="true" + command="cmd_save"> + <menupopup id="button-savePopup"> + <menuitem id="button-saveAsFile" + label="&saveAsFileCmd.label;" + accesskey="&saveAsFileCmd.accesskey;" + command="cmd_saveAsFile"/> + <menuseparator id="buttonSaveAfterFileSeparator"/> + <menuitem id="button-saveAsDraft" + label="&saveAsDraftCmd.label;" + accesskey="&saveAsDraftCmd.accesskey;" + command="cmd_saveAsDraft"/> + <menuitem id="button-saveAsTemplate" + label="&saveAsTemplateCmd.label;" + accesskey="&saveAsTemplateCmd.accesskey;" + command="cmd_saveAsTemplate"/> + </menupopup> + </toolbarbutton> + + <toolbaritem id="throbber-box"/> + </toolbar> + + <toolbarset id="customToolbars" context="toolbar-context-menu"/> + + <toolbar id="MsgHeadersToolbar" + persist="collapsed" + flex="1" + grippytooltiptext="&addressBar.tooltip;" + nowindowdrag="true"> + <hbox id="msgheaderstoolbar-box" flex="1"> + <vbox id="addresses-box" flex="1"> + <hbox align="center"> + <label value="&fromAddr.label;" accesskey="&fromAddr.accesskey;" control="msgIdentity"/> + <menulist id="msgIdentity" + editable="true" + disableautoselect="true" + flex="1" + oncommand="LoadIdentity(false);"> + <menupopup id="msgIdentityPopup"/> + </menulist> + </hbox> + <!-- Addressing Widget --> + <listbox id="addressingWidget" flex="1" + seltype="multiple" rows="4" + onkeydown="awKeyDown(event, this);" + onclick="awClickEmptySpace(event.originalTarget, true);" + ondragover="DragAddressOverTargetControl(event);" + ondrop="DropOnAddressingTarget(event, true);"> + + <listcols> + <listcol id="typecol-addressingWidget"/> + <listcol id="textcol-addressingWidget" flex="1"/> + </listcols> + + <listitem class="addressingWidgetItem" allowevents="true"> + <listcell class="addressingWidgetCell" align="stretch"> + <menulist id="addressCol1#1" disableonsend="true" + class="aw-menulist menulist-compact" flex="1" + onkeypress="awMenulistKeyPress(event, this);" + oncommand="onAddressColCommand(this.id);"> + <menupopup> + <menuitem value="addr_to" label="&toAddr.label;"/> + <menuitem value="addr_cc" label="&ccAddr.label;"/> + <menuitem value="addr_bcc" label="&bccAddr.label;"/> + <menuitem value="addr_reply" label="&replyAddr.label;"/> + <menuitem value="addr_newsgroups" + label="&newsgroupsAddr.label;"/> + <menuitem value="addr_followup" + label="&followupAddr.label;"/> + </menupopup> + </menulist> + </listcell> + + <listcell class="addressingWidgetCell"> + <textbox id="addressCol2#1" + class="plain textbox-addressingWidget uri-element" + aria-labelledby="addressCol1#1" + type="autocomplete" flex="1" maxrows="4" + newlines="replacewithcommas" + autocompletesearch="mydomain addrbook ldap news" + timeout="300" autocompletesearchparam="{}" + completedefaultindex="true" forcecomplete="true" + minresultsforpopup="2" ignoreblurwhilesearching="true" + ontextentered="awRecipientTextCommand(eventParam, this);" + onerrorcommand="awRecipientErrorCommand(eventParam, this);" + onchange="onRecipientsChanged();" + oninput="onRecipientsChanged();" + onkeypress="awRecipientKeyPress(event, this);" + onkeydown="awRecipientKeyDown(event, this);" + disableonsend="true"> + <image class="person-icon" + onclick="this.parentNode.select();"/> + </textbox> + </listcell> + </listitem> + </listbox> + <hbox align="center"> + <label value="&subject.label;" accesskey="&subject.accesskey;" control="msgSubject"/> + <textbox id="msgSubject" flex="1" class="toolbar" disableonsend="true" spellcheck="true" + oninput="gContentChanged=true;SetComposeWindowTitle();" + onkeypress="subjectKeyPress(event);" /> + </hbox> + </vbox> + <splitter id="attachmentbucket-sizer" collapse="after"/> + <vbox id="attachments-box"> + <label id="attachmentBucketText" value="&attachments.label;" crop="right" + accesskey="&attachments.accesskey;" control="attachmentBucket"/> + <listbox id="attachmentBucket" + seltype="multiple" + flex="1" + rows="4" + tabindex="-1" + context="msgComposeAttachmentContext" + disableoncustomize="true" + onkeypress="if (event.keyCode == 8 || event.keyCode == 46) RemoveSelectedAttachment();" + onclick="AttachmentBucketClicked(event);" + ondragover="attachmentBucketObserver.onDragOver(event);" + ondrop="attachmentBucketObserver.onDrop(event);" + ondragexit="attachmentBucketObserver.onDragExit(event);"/> + </vbox> + </hbox> + </toolbar> + + <!-- These toolbar items get filled out from the editorOverlay --> + <toolbar id="FormatToolbar" + class="chromeclass-toolbar" + persist="collapsed" + grippytooltiptext="&formatToolbar.tooltip;" + toolbarname="&showFormatToolbarCmd.label;" + accesskey="&showFormatToolbarCmd.accesskey;" + customizable="true" + defaultset="paragraph-select-container,font-face-select-container,color-buttons-container,DecreaseFontSizeButton,IncreaseFontSizeButton,separator,boldButton,italicButton,underlineButton,separator,ulButton,olButton,outdentButton,indentButton,separator,AlignPopupButton,InsertPopupButton,smileButtonMenu" + mode="icons" + iconsize="small" + defaultmode="icons" + defaulticonsize="small" + context="toolbar-context-menu" + nowindowdrag="true"> + <toolbaritem id="paragraph-select-container"/> + <toolbaritem id="font-face-select-container"/> + <toolbaritem id="color-buttons-container" + disableoncustomize="true"/> + <toolbarbutton id="DecreaseFontSizeButton"/> + <toolbarbutton id="IncreaseFontSizeButton"/> + <toolbarbutton id="boldButton"/> + <toolbarbutton id="italicButton"/> + <toolbarbutton id="underlineButton"/> + <toolbarbutton id="ulButton"/> + <toolbarbutton id="olButton"/> + <toolbarbutton id="outdentButton"/> + <toolbarbutton id="indentButton"/> + <toolbarbutton id="AlignPopupButton"/> + <toolbarbutton id="InsertPopupButton"/> + <toolbarbutton id="smileButtonMenu"/> + </toolbar> + + <toolbarpalette id="MsgComposeToolbarPalette"> + <toolbarbutton id="print-button" + label="&printButton.label;" + tooltiptext="&printButton.tooltip;"/> + <toolbarbutton id="button-security" + type="menu-button" + class="toolbarbutton-1" + label="&securityButton.label;" + tooltiptext="&securityButton.tooltip;" + oncommand="doSecurityButton();"> + <menupopup onpopupshowing="setSecuritySettings(2);"> + <menuitem id="menu_securityEncryptRequire2" + type="checkbox" + label="&menu_securityEncryptRequire.label;" + accesskey="&menu_securityEncryptRequire.accesskey;" + oncommand="setNextCommand('encryptMessage');"/> + <menuitem id="menu_securitySign2" + type="checkbox" + label="&menu_securitySign.label;" + accesskey="&menu_securitySign.accesskey;" + oncommand="setNextCommand('signMessage');"/> + <menuseparator id="smimeToolbarButtonSeparator"/> + <menuitem id="menu_securityStatus2" + label="&menu_securityStatus.label;" + accesskey="&menu_securityStatus.accesskey;" + oncommand="setNextCommand('show');"/> + </menupopup> + </toolbarbutton> + </toolbarpalette> + + </toolbox> + + <splitter id="compose-toolbar-sizer" + resizeafter="grow" + onmousedown="awSizerListen();" + oncommand="awSizerResized(this);"> + <observes element="MsgHeadersToolbar" attribute="collapsed"/> + </splitter> + + <!-- sidebar/toolbar/content/status --> + <hbox id="sidebar-parent" flex="1"> + <!-- From sidebarOverlay.xul --> + <vbox id="sidebar-box" class="chromeclass-extrachrome" hidden="true"/> + <splitter id="sidebar-splitter" class="chromeclass-extrachrome" hidden="true"/> + + <!-- The mail message body frame --> + <vbox id="appcontent" flex="1"> + <findbar id="FindToolbar" browserid="content-frame"/> + <editor id="content-frame" + type="content" + primary="true" + src="about:blank" + name="browser.message.body" + minheight="100" + flex="1" + ondblclick="EditorDblClick(event);" + context="contentAreaContextMenu"/> + </vbox> + </hbox> + + <hbox> + <notificationbox id="attachmentNotificationBox" + flex="1" + notificationside="bottom"/> + </hbox> + + <statusbar id="status-bar" + class="chromeclass-status"> + <statusbarpanel id="component-bar"/> + <statusbarpanel id="statusText" + flex="1"/> + <statusbarpanel id="statusbar-progresspanel" + class="statusbarpanel-progress" + collapsed="true"> + <progressmeter id="compose-progressmeter" + class="progressmeter-statusbar" + mode="normal" + value="0"/> + </statusbarpanel> + <statusbarpanel id="signing-status" + class="statusbarpanel-iconic" + collapsed="true" + oncommand="showMessageComposeSecurityStatus();"/> + <statusbarpanel id="encryption-status" + class="statusbarpanel-iconic" + collapsed="true" + oncommand="showMessageComposeSecurityStatus();"/> + <statusbarpanel id="offline-status" + class="statusbarpanel-iconic" + checkfunc="MailCheckBeforeOfflineChange();"/> + </statusbar> + +</window> diff --git a/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul b/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul new file mode 100644 index 0000000000..f3558873d2 --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<overlay id="msgComposeContextOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <menupopup id="contentAreaContextMenu" + onpopupshowing="return event.target != this || + openEditorContextMenu(this);"> + <!-- Hide the menuitems by default so they do not to show up + in the sidebar context menu. --> + <menuitem id="context-pasteNoFormatting" + insertafter="context-paste" + hidden="true" + command="cmd_pasteNoFormatting"/> + <menuitem id="context-pasteQuote" + insertafter="context-pasteNoFormatting" + hidden="true" + command="cmd_pasteQuote"/> + </menupopup> +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js new file mode 100644 index 0000000000..f67d919f63 --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js @@ -0,0 +1,30 @@ +/* -*- 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/. */ + +function Startup() { + let value = document.getElementById("mail.compose.autosave").value; + EnableElementById("autoSaveInterval", value, false); +} + +function EnableMailComposeAutosaveInterval(aValue) { + let focus = (document.getElementById("autoSave") == document.commandDispatcher.focusedElement); + EnableElementById("autoSaveInterval", aValue, focus); +} + +function PopulateFonts() { + var fontsList = document.getElementById("fontSelect"); + try { + var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"] + .getService(Ci.nsIFontEnumerator); + var localFonts = enumerator.EnumerateAllFonts(); + for (let font of localFonts) + if (font != "serif" && font != "sans-serif" && font != "monospace") + fontsList.appendItem(font, font); + } catch (ex) { } + + // Select the item after the list is completely generated. + document.getElementById(fontsList.getAttribute("preference")) + .setElementValue(fontsList); +} diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul new file mode 100644 index 0000000000..c6f3b4fac8 --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul @@ -0,0 +1,212 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % pref-composing_messagesDTD SYSTEM "chrome://messenger/locale/messengercompose/pref-composing_messages.dtd"> +%pref-composing_messagesDTD; +<!ENTITY % editorOverlayDTD SYSTEM "chrome://editor/locale/editorOverlay.dtd"> +%editorOverlayDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="composing_messages_pane" + label="&pref.composing.messages.title;" + script="chrome://messenger/content/messengercompose/pref-composing_messages.js" + onpaneload="this.PopulateFonts();"> + + <preferences id="composing_messages_preferences"> + <preference id="mail.forward_message_mode" + name="mail.forward_message_mode" + type="int"/> + <preference id="mail.reply_quote_inline" + name="mail.reply_quote_inline" + type="bool"/> + <preference id="mail.compose.autosave" + name="mail.compose.autosave" + type="bool" + onchange="EnableMailComposeAutosaveInterval(this.value);"/> + <preference id="mail.compose.autosaveinterval" + name="mail.compose.autosaveinterval" + type="int"/> + <preference id="mail.warn_on_send_accel_key" + name="mail.warn_on_send_accel_key" + type="bool"/> + <preference id="mailnews.wraplength" + name="mailnews.wraplength" + type="int"/> + <preference id="msgcompose.font_face" + name="msgcompose.font_face" + type="string"/> + <preference id="msgcompose.font_size" + name="msgcompose.font_size" + type="string"/> + <preference id="msgcompose.text_color" + name="msgcompose.text_color" + type="string"/> + <preference id="msgcompose.background_color" + name="msgcompose.background_color" + type="string"/> + <preference id="mailnews.reply_header_type" + name="mailnews.reply_header_type" + type="int"/> + <preference id="mail.compose.default_to_paragraph" + name="mail.compose.default_to_paragraph" + type="bool"/> + </preferences> + + <groupbox> + <caption label="&generalComposing.label;"/> + + <radiogroup id="forwardMessageMode" + orient="horizontal" + align="center" + preference="mail.forward_message_mode"> + <label value="&forwardMsg.label;" control="forwardMessageMode"/> + <radio value="2" + label="&inline.label;" + accesskey="&inline.accesskey;"/> + <radio value="0" + label="&asAttachment.label;" + accesskey="&asAttachment.accesskey;"/> + </radiogroup> + + <checkbox id="replyQuoteInline" label="&replyQuoteInline.label;" + preference="mail.reply_quote_inline" + accesskey="&replyQuoteInline.accesskey;"/> + + <hbox align="center"> + <checkbox id="autoSave" label="&autoSave.label;" + preference="mail.compose.autosave" + accesskey="&autoSave.accesskey;" + aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/> + <textbox id="autoSaveInterval" + type="number" + min="1" + max="99" + size="2" + preference="mail.compose.autosaveinterval" + aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/> + <label id="autoSaveEnd" value="&autoSaveEnd.label;"/> + </hbox> + + <checkbox id="mailWarnOnSendAccelKey" + label="&warnOnSendAccelKey.label;" + accesskey="&warnOnSendAccelKey.accesskey;" + preference="mail.warn_on_send_accel_key"/> + + <hbox align="center"> + <label id="wrapOutLabel" + value="&wrapOutMsg.label;" + accesskey="&wrapOutMsg.accesskey;" + control="wrapLength"/> + <textbox id="wrapLength" + type="number" + min="0" + max="999" + size="3" + preference="mailnews.wraplength" + aria-labelledby="wrapOutLabel wrapLength wrapOutEnd"/> + <label id="wrapOutEnd" value="&char.label;"/> + </hbox> + <hbox align="center"> + <label id="selectHeaderType" + value="&selectHeaderType.label;" + accesskey="&selectHeaderType.accesskey;" + control="mailNewsReplyList"/> + <menulist id="mailNewsReplyList" + preference="mailnews.reply_header_type"> + <menupopup> + <menuitem value="0" + label="&noReplyOption.label;"/> + <menuitem value="1" + label="&authorWroteOption.label;"/> + <menuitem value="2" + label="&onDateAuthorWroteOption.label;"/> + <menuitem value="3" + label="&authorWroteOnDateOption.label;"/> + </menupopup> + </menulist> + </hbox> + </groupbox> + + <!-- Composing Mail --> + + <groupbox align="start"> + <caption label="&defaultMessagesHeader.label;"/> + <grid> + <columns> + <column/> + <column/> + </columns> + + <rows> + <row align="center"> + <label value="&font.label;" + accesskey="&font.accesskey;" + control="fontSelect"/> + <menulist id="fontSelect" preference="msgcompose.font_face"> + <menupopup> + <menuitem value="" + label="&fontVarWidth.label;"/> + <menuitem value="tt" + label="&fontFixedWidth.label;"/> + <menuseparator/> + <menuitem value="Helvetica, Arial, sans-serif" + label="&fontHelvetica.label;"/> + <menuitem value="Times New Roman, Times, serif" + label="&fontTimes.label;"/> + <menuitem value="Courier New, Courier, monospace" + label="&fontCourier.label;"/> + <menuseparator/> + </menupopup> + </menulist> + </row> + <row align="center"> + <label value="&size.label;" + accesskey="&size.accesskey;" + control="fontSizeSelect"/> + <hbox align="center"> + <menulist id="fontSizeSelect" preference="msgcompose.font_size"> + <menupopup> + <menuitem value="x-small" label="&size-tinyCmd.label;"/> + <menuitem value="small" label="&size-smallCmd.label;"/> + <menuitem value="medium" label="&size-mediumCmd.label;"/> + <menuitem value="large" label="&size-largeCmd.label;"/> + <menuitem value="x-large" label="&size-extraLargeCmd.label;"/> + <menuitem value="xx-large" label="&size-hugeCmd.label;"/> + </menupopup> + </menulist> + <label value="&fontColor.label;" + accesskey="&fontColor.accesskey;" + control="msgComposeTextColor"/> + <colorpicker id="msgComposeTextColor" + type="button" + preference="msgcompose.text_color"/> + <label value="&bgColor.label;" + accesskey="&bgColor.accesskey;" + control="msgComposeBackgroundColor"/> + <colorpicker id="msgComposeBackgroundColor" + type="button" + preference="msgcompose.background_color"/> + </hbox> + </row> + </rows> + </grid> + <separator class="thin"/> + <description>&defaultCompose.label;</description> + <radiogroup id="defaultCompose" + class="indent" + preference="mail.compose.default_to_paragraph"> + <radio value="false" + label="&defaultBodyText.label;" + accesskey="&defaultBodyText.accesskey;"/> + <radio value="true" + label="&defaultParagraph.label;" + accesskey="&defaultParagraph.accesskey;"/> + </radiogroup> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js new file mode 100644 index 0000000000..b5c31d424d --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js @@ -0,0 +1,151 @@ +/* -*- 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 gListbox; +var gPref; +var gError; + +function Startup() +{ + // Store some useful elements in globals. + gListbox = + { + html: document.getElementById("html_domains"), + plaintext: document.getElementById("plaintext_domains") + }; + gPref = + { + html_domains: document.getElementById("mailnews.html_domains"), + plaintext_domains: document.getElementById("mailnews.plaintext_domains") + }; + gError = document.getElementById("formatting_error_msg"); + + // Make it easier to access the pref pane from onsync. + gListbox.html.pane = this; + gListbox.plaintext.pane = this; +} + +function AddDomain(aType) +{ + var domains = null; + var result = {value: null}; + if (Services.prompt.prompt(window, gListbox[aType].getAttribute("title"), + gListbox[aType].getAttribute("msg"), result, + null, {value: 0})) + domains = result.value.replace(/ /g, "").split(","); + + if (domains) + { + var added = false; + var removed = false; + var listbox = gListbox[aType]; + var other = aType == "html" ? gListbox.plaintext : gListbox.html; + for (var i = 0; i < domains.length; i++) + { + var domainName = TidyDomainName(domains[i], true); + if (domainName) + { + if (!DomainFirstMatch(listbox, domainName)) + { + var match = DomainFirstMatch(other, domainName); + if (match) + { + match.remove(); + removed = true; + } + listbox.appendItem(domainName); + added = true; + } + } + } + if (added) + listbox.doCommand(); + if (removed) + other.doCommand(); + } +} + +function TidyDomainName(aDomain, aWarn) +{ + // See if it is an email address and if so take just the domain part. + aDomain = aDomain.replace(/.*@/, ""); + + // See if it is a valid domain otherwise return null. + if (!/.\../.test(aDomain)) + { + if (aWarn) + { + var errorMsg = gError.getAttribute("inverr").replace(/@string@/, aDomain); + Services.prompt.alert(window, gError.getAttribute("title"), errorMsg); + } + return null; + } + + // Finally make sure the domain is in lowercase. + return aDomain.toLowerCase(); +} + +function DomainFirstMatch(aListbox, aDomain) +{ + return aListbox.getElementsByAttribute("label", aDomain).item(0); +} + +function RemoveDomains(aType, aEvent) +{ + if (aEvent && aEvent.keyCode != KeyEvent.DOM_VK_DELETE && + aEvent.keyCode != KeyEvent.DOM_VK_BACK_SPACE) + return; + + var nextNode = null; + var listbox = gListbox[aType]; + + while (listbox.selectedItem) + { + var selectedNode = listbox.selectedItem; + nextNode = selectedNode.nextSibling || selectedNode.previousSibling; + selectedNode.remove(); + } + + if (nextNode) + listbox.selectItem(nextNode); + + listbox.doCommand(); +} + +function ReadDomains(aListbox) +{ + var arrayOfPrefs = gPref[aListbox.id].value.replace(/ /g, "").split(","); + if (arrayOfPrefs) + { + var i; + // Check all the existing items, remove any that are not needed and + // make sure we do not duplicate any by removing from pref array. + var domains = aListbox.getElementsByAttribute("label", "*"); + if (domains) + { + for (i = domains.length; --i >= 0; ) + { + var domain = domains[i]; + var index = arrayOfPrefs.indexOf(domain.label); + if (index > -1) + arrayOfPrefs.splice(index, 1); + else + domain.remove(); + } + } + for (i = 0; i < arrayOfPrefs.length; i++) + { + var str = TidyDomainName(arrayOfPrefs[i], false); + if (str) + aListbox.appendItem(str); + } + } +} + +function WriteDomains(aListbox) +{ + var domains = aListbox.getElementsByAttribute("label", "*"); + return Array.from(domains, e => e.label).join(","); +} diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul new file mode 100644 index 0000000000..0167a0990f --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul @@ -0,0 +1,120 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/messengercompose/pref-formatting.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="formatting_pane" + label="&pref.formatting.title;" + script="chrome://messenger/content/messengercompose/pref-formatting.js"> + <preferences id="formatting_preferences"> + <preference id="mail.default_html_action" + name="mail.default_html_action" + type="int"/> + <preference id="mailnews.html_domains" + name="mailnews.html_domains" + type="string"/> + <preference id="mailnews.plaintext_domains" + name="mailnews.plaintext_domains" + type="string"/> + <preference id="mailnews.sendformat.auto_downgrade" + name="mailnews.sendformat.auto_downgrade" + type="bool"/> + </preferences> + + <data id="formatting_error_msg" + title="&domainnameError.title;" + inverr="&invalidEntryError.label;"/> + + <description>&sendMaildesc.label;</description> + + <radiogroup id="mailDefaultHTMLAction" + preference="mail.default_html_action"> + <radio value="0" + label="&askMe.label;" + accesskey="&askMe.accesskey;"/> + <radio value="1" + label="&convertPlain2.label;" + accesskey="&convertPlain2.accesskey;"/> + <radio value="2" + label="&sendHTML2.label;" + accesskey="&sendHTML2.accesskey;"/> + <radio value="3" + label="&sendBoth2.label;" + accesskey="&sendBoth2.accesskey;"/> + </radiogroup> + + <groupbox flex="1"> + <caption label="&domain.title;"/> + + <description>&domaindesc.label;</description> + + <hbox flex="1"> + <vbox flex="1"> + <label value="&HTMLdomaintitle.label;" + accesskey="&HTMLdomaintitle.accesskey;" + control="html_domains"/> + <hbox flex="1"> + <listbox id="html_domains" + title="&add.htmltitle;" + msg="&add.htmldomain;" + flex="1" + seltype="multiple" + preference="mailnews.html_domains" + onsyncfrompreference="return this.pane.ReadDomains(this);" + onsynctopreference="return this.pane.WriteDomains(this);" + onkeypress="RemoveDomains('html', event);"/> + <vbox> + <button label="&AddButton.label;" + accesskey="&AddHtmlDomain.accesskey;" + oncommand="AddDomain('html');"> + <observes element="html_domains" attribute="disabled"/> + </button> + <button label="&DeleteButton.label;" + accesskey="&DeleteHtmlDomain.accesskey;" + oncommand="RemoveDomains('html', null);"> + <observes element="html_domains" attribute="disabled"/> + </button> + </vbox> + </hbox> + </vbox> + <vbox flex="1"> + <label value="&PlainTexttitle.label;" + accesskey="&PlainTexttitle.accesskey;" + control="plaintext_domains"/> + <hbox flex="1"> + <listbox id="plaintext_domains" + title="&add.plaintexttitle;" + msg="&add.plaintextdomain;" + flex="1" + seltype="multiple" + preference="mailnews.plaintext_domains" + onsyncfrompreference="return this.pane.ReadDomains(this);" + onsynctopreference="return this.pane.WriteDomains(this);" + onkeypress="RemoveDomains('plaintext', event);"/> + <vbox> + <button label="&AddButton.label;" + accesskey="&AddPlainText.accesskey;" + oncommand="AddDomain('plaintext');"> + <observes element="plaintext_domains" attribute="disabled"/> + </button> + <button label="&DeleteButton.label;" + accesskey="&DeletePlainText.accesskey;" + oncommand="RemoveDomains('plaintext', null);"> + <observes element="plaintext_domains" attribute="disabled"/> + </button> + </vbox> + </hbox> + </vbox> + </hbox> + </groupbox> + + <checkbox id="autoDowngrade" + label="&autoDowngrade.label;" + accesskey="&autoDowngrade.accesskey;" + preference="mailnews.sendformat.auto_downgrade"/> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/compose/jar.mn b/comm/suite/mailnews/components/compose/jar.mn new file mode 100644 index 0000000000..c9465fa8d7 --- /dev/null +++ b/comm/suite/mailnews/components/compose/jar.mn @@ -0,0 +1,14 @@ +# 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/. + +messenger.jar: + content/messenger/messengercompose/pref-composing_messages.xul (content/prefs/pref-composing_messages.xul) + content/messenger/messengercompose/pref-composing_messages.js (content/prefs/pref-composing_messages.js) + content/messenger/messengercompose/pref-formatting.xul (content/prefs/pref-formatting.xul) + content/messenger/messengercompose/pref-formatting.js (content/prefs/pref-formatting.js) +* content/messenger/messengercompose/messengercompose.xul (content/messengercompose.xul) + content/messenger/messengercompose/mailComposeOverlay.xul (content/mailComposeOverlay.xul) + content/messenger/messengercompose/msgComposeContextOverlay.xul (content/msgComposeContextOverlay.xul) + content/messenger/messengercompose/MsgComposeCommands.js (content/MsgComposeCommands.js) + content/messenger/messengercompose/addressingWidgetOverlay.js (content/addressingWidgetOverlay.js) diff --git a/comm/suite/mailnews/components/compose/moz.build b/comm/suite/mailnews/components/compose/moz.build new file mode 100644 index 0000000000..de5cd1bf81 --- /dev/null +++ b/comm/suite/mailnews/components/compose/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] |