/* 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/. */ /** * Functions related to displaying the headers for a selected message in the * message pane. */ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource:///modules/displayNameUtils.js"); Components.utils.import("resource:///modules/mailServices.js"); Components.utils.import("resource:///modules/gloda/utils.js"); //////////////////////////////////////////////////////////////////////////////////// // Warning: It's critical that the code in here for displaying the message // headers for a selected message remain as fast as possible. In particular, // right now, we only introduce one reflow per message. i.e. if you click on // a message in the thread pane, we batch up all the changes for displaying // the header pane (to, cc, attachements button, etc.) and we make a single // pass to display them. It's critical that we maintain this one reflow per // message view in the message header pane. //////////////////////////////////////////////////////////////////////////////////// var gViewAllHeaders = false; var gMinNumberOfHeaders = 0; var gDummyHeaderIdIndex = 0; var gBuildAttachmentsForCurrentMsg = false; var gBuiltExpandedView = false; var gHeadersShowReferences = false; /** * Show the friendly display names for people I know, * instead of the name + email address. */ var gShowCondensedEmailAddresses; /** * Other components may listen to on start header & on end header notifications * for each message we display: to do that you need to add yourself to our * gMessageListeners array with an object that supports the three properties: * onStartHeaders, onEndHeaders and onEndAttachments. * * Additionally, if your object has an onBeforeShowHeaderPane() method, it will * be called at the appropriate time. This is designed to give add-ons a * chance to examine and modify the currentHeaderData array before it gets * displayed. */ var gMessageListeners = new Array(); /** * This expanded header view shows many of the more common (and useful) headers. * * For every possible "view" in the message pane, you need to define the header * names you want to see in that view. In addition, include information * describing how you want that header field to be presented. i.e. if it's an * email address field, if you want a toggle inserted on the node in case * of multiple email addresses, etc. We'll then use this static table to * dynamically generate header view entries which manipulate the UI. * When you add a header to one of these view lists you can specify * the following properties: * name: the name of the header. i.e. "to", "subject". This must be in * lower case and the name of the header is used to help * dynamically generate ids for objects in the document. (REQUIRED) * useToggle: true if the values for this header are multiple email * addresses and you want a (more) toggle to show a short * vs. long list (DEFAULT: false) * outputFunction: this is a method which takes a headerEntry (see the definition * below) and a header value. This allows you to provide your own * methods for actually determining how the header value * is displayed. (DEFAULT: updateHeaderValue which just sets the * header value on the text node) */ var gExpandedHeaderList = [ { name: "subject" }, { name: "from", useToggle:true, outputFunction: OutputEmailAddresses }, { name: "reply-to", useToggle:true, outputFunction: OutputEmailAddresses }, { name: "to", useToggle:true, outputFunction: OutputEmailAddresses }, { name: "cc", useToggle:true, outputFunction: OutputEmailAddresses }, { name: "bcc", useToggle:true, outputFunction: OutputEmailAddresses }, { name: "newsgroups", outputFunction: OutputNewsgroups }, { name: "references", outputFunction: OutputMessageIds }, { name: "followup-to", outputFunction: OutputNewsgroups }, { name: "content-base" }, { name: "tags" } ]; /** * These are all the items that use a mail-multi-emailHeaderField widget and * therefore may require updating if the address book changes. */ var gEmailAddressHeaderNames = ["from", "reply-to", "to", "cc", "bcc", "toCcBcc"]; /** * Now, for each view the message pane can generate, we need a global table of * headerEntries. These header entry objects are generated dynamically based on * the static data in the header lists (see above) and elements we find in the * DOM based on properties in the header lists. */ var gExpandedHeaderView = {}; /** * This is an array of header name and value pairs for the currently displayed * message. It's purely a data object and has no view information. View * information is contained in the view objects. * For a given entry in this array you can ask for: * .headerName name of the header (i.e. 'to'). Always stored in lower case * .headerValue value of the header "johndoe@example.com" */ var currentHeaderData = {}; /** * CurrentAttachments is an array of AttachmentInfo objects. */ var currentAttachments = new Array(); var nsIAbDirectory = Components.interfaces.nsIAbDirectory; var nsIAbListener = Components.interfaces.nsIAbListener; var nsIAbCard = Components.interfaces.nsIAbCard; /** * Our constructor method which creates a header Entry based on an entry * in one of the header lists. A header entry is different from a header list. * A header list just describes how you want a particular header to be * presented. The header entry actually has knowledge about the DOM * and the actual DOM elements associated with the header. * * @param prefix the name of the view (e.g. "expanded") * @param headerListInfo entry from a header list. */ function createHeaderEntry(prefix, headerListInfo) { var partialIDName = prefix + headerListInfo.name; this.enclosingBox = document.getElementById(partialIDName + "Box"); this.enclosingRow = document.getElementById(partialIDName + "Row"); this.textNode = document.getElementById(partialIDName + "Value"); this.isNewHeader = false; this.valid = false; if ("useToggle" in headerListInfo) { this.useToggle = headerListInfo.useToggle; if (this.useToggle) { // find the toggle icon in the document this.toggleIcon = this.enclosingBox.toggleIcon; this.longTextNode = this.enclosingBox.longEmailAddresses; this.textNode = this.enclosingBox.emailAddresses; } } else this.useToggle = false; if ("outputFunction" in headerListInfo) this.outputFunction = headerListInfo.outputFunction; else this.outputFunction = updateHeaderValue; // Stash this so that the binding can // later attach it to any tags it creates for later // extraction and use by UpdateEmailNodeDetails. this.enclosingBox.headerName = headerListInfo.name; } function initializeHeaderViewTables() { // Iterate over each header in our header list arrays and create header entries // for each one. These header entries are then stored in the appropriate header // table. var index; for (index = 0; index < gExpandedHeaderList.length; index++) { var headerName = gExpandedHeaderList[index].name; gExpandedHeaderView[headerName] = new createHeaderEntry("expanded", gExpandedHeaderList[index]); } var extraHeaders = Services.prefs.getCharPref("mailnews.headers.extraExpandedHeaders").split(" "); for (index = 0; index < extraHeaders.length; index++) { var extraHeader = extraHeaders[index]; gExpandedHeaderView[extraHeader.toLowerCase()] = new HeaderView(extraHeader, extraHeader); } if (Services.prefs.getBoolPref("mailnews.headers.showOrganization")) { var organizationEntry = { name: "organization", outputFunction: updateHeaderValue }; gExpandedHeaderView[organizationEntry.name] = new createHeaderEntry("expanded", organizationEntry); } if (Services.prefs.getBoolPref("mailnews.headers.showUserAgent")) { var userAgentEntry = { name: "user-agent", outputFunction: updateHeaderValue }; gExpandedHeaderView[userAgentEntry.name] = new createHeaderEntry("expanded", userAgentEntry); } if (Services.prefs.getBoolPref("mailnews.headers.showMessageId")) { var messageIdEntry = { name: "message-id", outputFunction: OutputMessageIds }; gExpandedHeaderView[messageIdEntry.name] = new createHeaderEntry("expanded", messageIdEntry); } if (Services.prefs.getBoolPref("mailnews.headers.showSender")) { var senderEntry = { name: "sender", outputFunction: OutputEmailAddresses }; gExpandedHeaderView[senderEntry.name] = new createHeaderEntry("expanded", senderEntry); } } function OnLoadMsgHeaderPane() { // HACK...force our XBL bindings file to be load before we try to create our // first xbl widget.... otherwise we have problems. document.loadBindingDocument("chrome://messenger/content/mailWidgets.xml"); // Load any preferences that at are global with regards to // displaying a message... gMinNumberOfHeaders = Services.prefs.getIntPref("mailnews.headers.minNumHeaders"); gShowCondensedEmailAddresses = Services.prefs.getBoolPref("mail.showCondensedAddresses"); gHeadersShowReferences = Services.prefs.getBoolPref("mailnews.headers.showReferences"); // listen to the Services.prefs.addObserver("mail.showCondensedAddresses", MsgHdrViewObserver, false); Services.prefs.addObserver("mailnews.headers.showReferences", MsgHdrViewObserver, false); Services.prefs.addObserver("mailnews.header.toolbar", MsgHdrViewObserver, false); initializeHeaderViewTables(); // Add an address book listener so we can update the header view when things // change. MailServices.ab.addAddressBookListener(AddressBookListener, Components.interfaces.nsIAbListener.all); // If an invalid index is selected; reset to 0. One way this can happen // is if a value of 1 was persisted to localStore.rdf by Tb2 (when there were // two panels), and then the user upgraded to Tb3, which only has one. // Presumably this can also catch cases of extension uninstalls as well. let deckElement = document.getElementById("msgHeaderViewDeck") // If the selectedIndex was 0, then we were using the compact header, (if we // were coming from TB2, but we'll check that in the feature configurator). deckElement.usedCompactHeader = (deckElement.selectedIndex == 0); if (deckElement.selectedIndex < 0 || deckElement.selectedIndex >= deckElement.childElementCount) { deckElement.selectedIndex = 0; } initToolbarMenu(); // Only offer openInTab and openInNewWindow if this window supports tabs... // (i.e. is not a standalone message window), since those actions are likely // to be significantly less common in that case. if (document.getElementById("otherActionsOpenIn")) { let opensAreHidden = document.getElementById("tabmail") ? false : true; document.getElementById("otherActionsOpenIn").hidden = opensAreHidden; } // Dispatch an event letting any listeners know that we have loaded // the message pane. var headerViewElement = document.getElementById("msgHeaderView"); headerViewElement.dispatchEvent(new Event("messagepane-loaded", { bubbles: false, cancelable: true })); initInlineToolbox("header-view-toolbox", "header-view-toolbar", "CustomizeHeaderToolbar", function() { UpdateJunkButton(); UpdateReplyButtons(); }); initInlineToolbox("attachment-view-toolbox", "attachment-view-toolbar", "CustomizeAttachmentToolbar", function () { updateSaveAllAttachmentsButton(); }); top.controllers.appendController(AttachmentMenuController); } /** * Initialize an inline toolbox and its toolbar to have the appropriate * attributes necessary for customization and persistence. * * @param toolboxId the id for the toolbox to initialize * @param toolbarId the id for the toolbar to initialize * @param popupId the id for the menupopup to initialize * @param customizeChange (optional) a function to call when a toolbar button * has been added or removed from the toolbar */ function initInlineToolbox(toolboxId, toolbarId, popupId, customizeChange) { var headerBox = document.getElementById("msgHeaderView"); headerBox.setAttribute("showToolbar", Services.prefs.getBoolPref("mailnews.header.toolbar")); let toolbox = document.getElementById(toolboxId); toolbox.customizeDone = function(aEvent) { MailToolboxCustomizeDone(aEvent, popupId); }; if (customizeChange) toolbox.customizeChange = customizeChange; let toolbarset = document.getElementById("customToolbars"); toolbox.toolbarset = toolbarset; // Check whether we did an upgrade to a customizable header pane. // If yes, set the header pane toolbar mode to icons besides text let toolbar = document.getElementById(toolbarId); if (toolbox && toolbar) { if (!toolbox.getAttribute("mode")) { /* set toolbox attributes to default values */ let mode = toolbox.getAttribute("defaultmode"); let align = toolbox.getAttribute("defaultlabelalign"); let iconsize = toolbox.getAttribute("defaulticonsize"); toolbox.setAttribute("mode", mode); toolbox.setAttribute("labelalign", align); toolbox.setAttribute("iconsize", iconsize); toolbox.ownerDocument.persist(toolbox.id, "mode"); toolbox.ownerDocument.persist(toolbox.id, "iconsize"); toolbox.ownerDocument.persist(toolbox.id, "labelalign"); /* set toolbar attributes to default values */ iconsize = toolbar.getAttribute("defaulticonsize"); toolbar.setAttribute("iconsize", iconsize); toolbar.ownerDocument.persist(toolbar.id, "iconsize"); } } } function initToolbarMenu() { // Get the mode as persisted on the toolbar itself. let mode = document.getElementById("header-view-toolbar") .getAttribute("mode"); return; } function OnUnloadMsgHeaderPane() { Services.prefs.removeObserver("mail.showCondensedAddresses", MsgHdrViewObserver); Services.prefs.removeObserver("mailnews.headers.showReferences", MsgHdrViewObserver); Services.prefs.removeObserver("mailnews.header.toolbar", MsgHdrViewObserver); MailServices.ab.removeAddressBookListener(AddressBookListener); // dispatch an event letting any listeners know that we have unloaded // the message pane var headerViewElement = document.getElementById("msgHeaderView"); headerViewElement.dispatchEvent(new Event("messagepane-unloaded", { bubbles: false, cancelable: true })); } var MsgHdrViewObserver = { observe: function(subject, topic, prefName) { // verify that we're changing the mail pane config pref if (topic == "nsPref:changed") { if (prefName == "mail.showCondensedAddresses") { gShowCondensedEmailAddresses = Services.prefs.getBoolPref("mail.showCondensedAddresses"); ReloadMessage(); } else if (prefName == "mailnews.headers.showReferences") { gHeadersShowReferences = Services.prefs.getBoolPref("mailnews.headers.showReferences"); ReloadMessage(); } else if (prefName == "mailnews.header.toolbar") { var headerBox = document.getElementById("msgHeaderView"); headerBox.setAttribute("showToolbar", Services.prefs.getBoolPref("mailnews.header.toolbar")); } } } }; var AddressBookListener = { onItemAdded: function(aParentDir, aItem) { OnAddressBookDataChanged(nsIAbListener.itemAdded, aParentDir, aItem); }, onItemRemoved: function(aParentDir, aItem) { OnAddressBookDataChanged(aItem instanceof nsIAbCard ? nsIAbListener.directoryItemRemoved : nsIAbListener.directoryRemoved, aParentDir, aItem); }, onItemPropertyChanged: function(aItem, aProperty, aOldValue, aNewValue) { // We only need updates for card changes, address book and mailing list // ones don't affect us here. if (aItem instanceof Components.interfaces.nsIAbCard) OnAddressBookDataChanged(nsIAbListener.itemChanged, null, aItem); } }; function OnAddressBookDataChanged(aAction, aParentDir, aItem) { gEmailAddressHeaderNames.forEach(function (headerName) { let headerEntry = null; if (headerName in gExpandedHeaderView) { headerEntry = gExpandedHeaderView[headerName]; if (headerEntry) headerEntry.enclosingBox.updateExtraAddressProcessing(aAction, aParentDir, aItem); } }); } /** * The messageHeaderSink is the class that gets notified of a message's headers * as we display the message through our mime converter. */ var messageHeaderSink = { QueryInterface: XPCOMUtils.generateQI( [Components.interfaces.nsIMsgHeaderSink]), onStartHeaders: function() { this.mSaveHdr = null; // Every time we start to redisplay a message, check the view all headers // pref... var showAllHeadersPref = Services.prefs.getIntPref("mail.show_headers"); if (showAllHeadersPref == 2) { gViewAllHeaders = true; } else { if (gViewAllHeaders) { // If we currently are in view all header mode, rebuild our header // view so we remove most of the header data. hideHeaderView(gExpandedHeaderView); RemoveNewHeaderViews(gExpandedHeaderView); gDummyHeaderIdIndex = 0; gExpandedHeaderView = {}; initializeHeaderViewTables(); } gViewAllHeaders = false; } ClearCurrentHeaders(); gBuiltExpandedView = false; gBuildAttachmentsForCurrentMsg = false; ClearAttachmentList(); gMessageNotificationBar.clearMsgNotifications(); for (let index in gMessageListeners) gMessageListeners[index].onStartHeaders(); }, onEndHeaders: function() { // Give add-ons a chance to modify currentHeaderData before it actually // gets displayed. for (let index in gMessageListeners) if ("onBeforeShowHeaderPane" in gMessageListeners[index]) gMessageListeners[index].onBeforeShowHeaderPane(); // Load feed web page if so configured. This entry point works for // messagepane loads in 3pane folder tab, 3pane message tab, and the // standalone message window. if (!FeedMessageHandler.shouldShowSummary(gMessageDisplay.displayedMessage, false)) FeedMessageHandler.setContent(gMessageDisplay.displayedMessage, false); ShowMessageHeaderPane(); // WARNING: This is the ONLY routine inside of the message Header Sink // that should trigger a reflow! ClearHeaderView(gExpandedHeaderView); // Make sure there is a subject even if it's empty so we'll show the // subject and the twisty. EnsureSubjectValue(); // Only update the expanded view if it's actually selected (an // extension-provided panel could be visible instead) and needs updating. if (document.getElementById("msgHeaderViewDeck").selectedIndex == 0 && !gBuiltExpandedView) { UpdateExpandedMessageHeaders(); } gMessageNotificationBar.setDraftEditMessage(); UpdateJunkButton(); for (let index in gMessageListeners) gMessageListeners[index].onEndHeaders(); }, processHeaders: function(headerNameEnumerator, headerValueEnumerator, dontCollectAddress) { this.onStartHeaders(); const kMailboxSeparator = ", "; var index = 0; while (headerNameEnumerator.hasMore()) { var header = new Object; header.headerValue = headerValueEnumerator.getNext(); header.headerName = headerNameEnumerator.getNext(); // For consistency's sake, let us force all header names to be lower // case so we don't have to worry about looking for: Cc and CC, etc. var lowerCaseHeaderName = header.headerName.toLowerCase(); // If we have an x-mailer, x-mimeole, or x-newsreader string, // put it in the user-agent slot which we know how to handle already. if (/^x-(mailer|mimeole|newsreader)$/.test(lowerCaseHeaderName)) lowerCaseHeaderName = "user-agent"; if (this.mDummyMsgHeader) { if (lowerCaseHeaderName == "from") this.mDummyMsgHeader.author = header.headerValue; else if (lowerCaseHeaderName == "to") this.mDummyMsgHeader.recipients = header.headerValue; else if (lowerCaseHeaderName == "cc") this.mDummyMsgHeader.ccList = header.headerValue; else if (lowerCaseHeaderName == "subject") this.mDummyMsgHeader.subject = header.headerValue; else if (lowerCaseHeaderName == "reply-to") this.mDummyMsgHeader.replyTo = header.headerValue; else if (lowerCaseHeaderName == "message-id") this.mDummyMsgHeader.messageId = header.headerValue; else if (lowerCaseHeaderName == "list-post") this.mDummyMsgHeader.listPost = header.headerValue; else if (lowerCaseHeaderName == "delivered-to") this.mDummyMsgHeader.deliveredTo = header.headerValue; else if (lowerCaseHeaderName == "date") this.mDummyMsgHeader.date = Date.parse(header.headerValue) * 1000; } // according to RFC 2822, certain headers // can occur "unlimited" times if (lowerCaseHeaderName in currentHeaderData) { // Sometimes, you can have multiple To or Cc lines.... // In this case, we want to append these headers into one. if (lowerCaseHeaderName == "to" || lowerCaseHeaderName == "cc") { currentHeaderData[lowerCaseHeaderName].headerValue = currentHeaderData[lowerCaseHeaderName].headerValue + "," + header.headerValue; } else { // Use the index to create a unique header name like: // received5, received6, etc currentHeaderData[lowerCaseHeaderName + index++] = header; } } else currentHeaderData[lowerCaseHeaderName] = header; } // while we have more headers to parse // Process message tags as if they were headers in the message. SetTagHeader(); if (("from" in currentHeaderData) && ("sender" in currentHeaderData)) { var senderMailbox = kMailboxSeparator + MailServices.headerParser.extractHeaderAddressMailboxes( currentHeaderData.sender.headerValue) + kMailboxSeparator; var fromMailboxes = kMailboxSeparator + MailServices.headerParser.extractHeaderAddressMailboxes( currentHeaderData.from.headerValue) + kMailboxSeparator; if (fromMailboxes.includes(senderMailbox)) delete currentHeaderData.sender; } // We don't need to show the reply-to header if its value is either // the From field (totally pointless) or the To field (common for // mailing lists, but not that useful). if (("from" in currentHeaderData) && ("to" in currentHeaderData) && ("reply-to" in currentHeaderData)) { var replyToMailbox = MailServices.headerParser.extractHeaderAddressMailboxes( currentHeaderData["reply-to"].headerValue); var fromMailboxes = MailServices.headerParser.extractHeaderAddressMailboxes( currentHeaderData.from.headerValue); var toMailboxes = MailServices.headerParser.extractHeaderAddressMailboxes( currentHeaderData.to.headerValue); if (replyToMailbox == fromMailboxes || replyToMailbox == toMailboxes) delete currentHeaderData["reply-to"]; } let expandedfromLabel = document.getElementById("expandedfromLabel"); if (gFolderDisplay.selectedMessageIsFeed) expandedfromLabel.value = expandedfromLabel.getAttribute("valueAuthor"); else expandedfromLabel.value = expandedfromLabel.getAttribute("valueFrom"); this.onEndHeaders(); }, handleAttachment: function(contentType, url, displayName, uri, isExternalAttachment) { this.skipAttachment = true; // Don't show vcards as external attachments in the UI. libmime already // renders them inline. if (!this.mSaveHdr) this.mSaveHdr = messenger.messageServiceFromURI(uri) .messageURIToMsgHdr(uri); if (contentType == "text/x-vcard") { var inlineAttachments = Services.prefs.getBoolPref("mail.inline_attachments"); var displayHtmlAs = Services.prefs.getIntPref("mailnews.display.html_as"); if (inlineAttachments && !displayHtmlAs) return; } var size = null; if (isExternalAttachment && url.startsWith("file:")) { let fileHandler = Services.io.getProtocolHandler("file") .QueryInterface(Components.interfaces.nsIFileProtocolHandler); try { let file = fileHandler.getFileFromURLSpec(url); // Can't get size for detached attachments which are no longer // available on the specified location. if (file.exists()) size = file.fileSize; } catch(e) { Components.utils.reportError("Couldn't open external attachment; " + "url=" + url + "; " + e); } } currentAttachments.push(new AttachmentInfo(contentType, url, displayName, uri, isExternalAttachment, size)); this.skipAttachment = false; // If we have an attachment, set the nsMsgMessageFlags.Attachment flag // on the hdr to cause the "message with attachment" icon to show up // in the thread pane. // We only need to do this on the first attachment. var numAttachments = currentAttachments.length; if (numAttachments == 1) { // We also have to enable the Message/Attachments menuitem. var node = document.getElementById("msgAttachmentMenu"); if (node) node.removeAttribute("disabled"); // convert the uri into a hdr this.mSaveHdr.markHasAttachments(true); // we also do the same on appmenu let appmenunode = document.getElementById("appmenu_msgAttachmentMenu"); if (appmenunode) appmenunode.removeAttribute("disabled"); // convert the uri into a hdr this.mSaveHdr.markHasAttachments(true); } }, addAttachmentField: function(field, value) { if (this.skipAttachment) return; let last = currentAttachments[currentAttachments.length - 1]; if (field == "X-Mozilla-PartSize" && !last.url.startsWith("file") && !last.isDeleted) { let size = parseInt(value); if (last.isExternalAttachment && last.url.startsWith("http")) { // Check if an external link attachment's reported size is sane. // A size of < 2 isn't sensical so ignore such placeholder values. // Don't accept a size with any non numerics. Also cap the number. if (isNaN(size) || size.toString().length != value.length || size < 2) size = -1; if (size > Number.MAX_SAFE_INTEGER) size = Number.MAX_SAFE_INTEGER; } // libmime returns -1 if it never managed to figure out the size. if (size != -1) last.size = size; } else if (field == "X-Mozilla-PartDownloaded" && value == "0") { // We haven't downloaded the attachment, so any size we get from // libmime is almost certainly inaccurate. Just get rid of it. (Note: // this relies on the fact that PartDownloaded comes after PartSize from // the MIME emitter.) last.size = null; } }, onEndAllAttachments: function() { displayAttachmentsForExpandedView(); for (let listener of gMessageListeners) { if ("onEndAttachments" in listener) listener.onEndAttachments(); } }, /** * This event is generated by nsMsgStatusFeedback when it gets an * OnStateChange event for STATE_STOP. This is the same event that * generates the "msgLoaded" property flag change event. This best * corresponds to the end of the streaming process. */ onEndMsgDownload: function(url) { gMessageDisplay.onLoadCompleted(); let expanded = Services.prefs.getBoolPref( "mailnews.attachments.display.start_expanded"); if (expanded) toggleAttachmentList(true); // if we don't have any attachments, turn off the attachments flag if (!this.mSaveHdr) { var messageUrl = url.QueryInterface(Components.interfaces.nsIMsgMessageUrl); this.mSaveHdr = messenger.msgHdrFromURI(messageUrl.uri); } if (!currentAttachments.length && this.mSaveHdr) this.mSaveHdr.markHasAttachments(false); let browser = getBrowser(); if (currentAttachments.length && Services.prefs.getBoolPref("mail.inline_attachments") && this.mSaveHdr && gFolderDisplay.selectedMessageIsFeed && browser && browser.contentDocument && browser.contentDocument.body) { for (let img of browser.contentDocument.body.getElementsByClassName("moz-attached-image")) { for (let attachment of currentAttachments) { let partID = img.src.split("&part=")[1]; partID = partID ? partID.split("&")[0] : null; if (attachment.partID && partID == attachment.partID) { img.src = attachment.url; break; } } img.addEventListener("load", function(event) { if (this.clientWidth > this.parentNode.clientWidth) { img.setAttribute("overflowing", "true"); img.setAttribute("shrinktofit", "true"); } }); } } OnMsgParsed(url); }, onEndMsgHeaders: function(url) { OnMsgLoaded(url); }, onMsgHasRemoteContent: function(aMsgHdr, aContentURI, aCanOverride) { gMessageNotificationBar.setRemoteContentMsg(aMsgHdr, aContentURI, aCanOverride); }, mSecurityInfo : null, mSaveHdr: null, get securityInfo() { return this.mSecurityInfo; }, set securityInfo(aSecurityInfo) { this.mSecurityInfo = aSecurityInfo; }, mDummyMsgHeader: null, get dummyMsgHeader() { if (!this.mDummyMsgHeader) this.mDummyMsgHeader = new nsDummyMsgHeader(); // The URI resolution will never work on the dummy header; // save it now... we know it will be needed eventually. // (And save it every time we come through here, not just when // we create it; the onStartHeaders might come after creation!) this.mSaveHdr = this.mDummyMsgHeader; return this.mDummyMsgHeader; }, mProperties: null, get properties() { if (!this.mProperties) this.mProperties = Components.classes["@mozilla.org/hash-property-bag;1"]. createInstance(Components.interfaces.nsIWritablePropertyBag2); return this.mProperties; }, resetProperties: function() { this.mProperties = null; } }; function SetTagHeader() { // It would be nice if we passed in the msgHdr from the back end. var msgHdr = gFolderDisplay.selectedMessage; if (!msgHdr) return; // no msgHdr to add our tags to // get the list of known tags var tagArray = MailServices.tags.getAllTags({}); var tagKeys = {}; for (var tagInfo of tagArray) if (tagInfo.tag) tagKeys[tagInfo.key] = true; // extract the tag keys from the msgHdr var msgKeyArray = msgHdr.getStringProperty("keywords").split(" "); // attach legacy label to the front if not already there var label = msgHdr.label; if (label) { let labelKey = "$label" + label; if (!msgKeyArray.includes(labelKey)) msgKeyArray.unshift(labelKey); } // Rebuild the keywords string with just the keys that are actual tags or // legacy labels and not other keywords like Junk and NonJunk. // Retain their order, though, with the label as oldest element. for (let i = msgKeyArray.length - 1; i >= 0; --i) if (!(msgKeyArray[i] in tagKeys)) msgKeyArray.splice(i, 1); // remove non-tag key var msgKeys = msgKeyArray.join(" "); if (msgKeys) currentHeaderData.tags = {headerName: "tags", headerValue: msgKeys}; else // no more tags, so clear out the header field delete currentHeaderData.tags; } function EnsureSubjectValue() { if (!("subject" in currentHeaderData)) { let foo = new Object; foo.headerValue = ""; foo.headerName = "subject"; currentHeaderData[foo.headerName] = foo; } } function OnTagsChange() { // rebuild the tag headers SetTagHeader(); // Now update the expanded header view to rebuild the tags, // and then show or hide the tag header box. if (gBuiltExpandedView) { let headerEntry = gExpandedHeaderView.tags; if (headerEntry) { headerEntry.valid = ("tags" in currentHeaderData); if (headerEntry.valid) headerEntry.outputFunction(headerEntry, currentHeaderData.tags.headerValue); // we may need to collapse or show the tag header row... headerEntry.enclosingRow.collapsed = !headerEntry.valid; // ... and ensure that all headers remain correctly aligned syncGridColumnWidths(); } } } /** * Flush out any local state being held by a header entry for a given table. * * @param aHeaderTable Table of header entries */ function ClearHeaderView(aHeaderTable) { for (let name in aHeaderTable) { let headerEntry = aHeaderTable[name]; if (headerEntry.enclosingBox.clearHeaderValues) headerEntry.enclosingBox.clearHeaderValues(); headerEntry.valid = false; } } /** * Make sure that any valid header entry in the table is collapsed. * * @param aHeaderTable Table of header entries */ function hideHeaderView(aHeaderTable) { for (let name in aHeaderTable) { let headerEntry = aHeaderTable[name]; headerEntry.enclosingRow.collapsed = true; } } /** * Make sure that any valid header entry in the table specified is visible. * * @param aHeaderTable Table of header entries */ function showHeaderView(aHeaderTable) { for (let name in aHeaderTable) { let headerEntry = aHeaderTable[name]; if (headerEntry.valid) { headerEntry.enclosingRow.collapsed = false; } else { // if the entry is invalid, always make sure it's collapsed headerEntry.enclosingRow.collapsed = true; } } } /** * Enumerate through the list of headers and find the number that are visible * add empty entries if we don't have the minimum number of rows. */ function EnsureMinimumNumberOfHeaders (headerTable) { // 0 means we don't have a minimum... do nothing special if (!gMinNumberOfHeaders) return; var numVisibleHeaders = 0; for (let name in headerTable) { let headerEntry = headerTable[name]; if (headerEntry.valid) numVisibleHeaders ++; } if (numVisibleHeaders < gMinNumberOfHeaders) { // How many empty headers do we need to add? var numEmptyHeaders = gMinNumberOfHeaders - numVisibleHeaders; // We may have already dynamically created our empty rows and we just need // to make them visible. for (let index in headerTable) { let headerEntry = headerTable[index]; if (index.startsWith("Dummy-Header") && numEmptyHeaders) { headerEntry.valid = true; numEmptyHeaders--; } } // Ok, now if we have any extra dummy headers we need to add, create a new // header widget for them. while (numEmptyHeaders) { var dummyHeaderId = "Dummy-Header" + gDummyHeaderIdIndex; gExpandedHeaderView[dummyHeaderId] = new HeaderView(dummyHeaderId, ""); gExpandedHeaderView[dummyHeaderId].valid = true; gDummyHeaderIdIndex++; numEmptyHeaders--; } } } /** * Make sure the appropriate fields in the expanded header view are collapsed * or visible... */ function updateExpandedView() { // If the expanded view isn't selected, don't bother updating it. if (document.getElementById("msgHeaderViewDeck").selectedIndex != 0) return; if (gMinNumberOfHeaders) EnsureMinimumNumberOfHeaders(gExpandedHeaderView); showHeaderView(gExpandedHeaderView); // Now that we have all the headers, ensure that the name columns of both // grids are the same size so that they don't look weird. syncGridColumnWidths(); UpdateJunkButton(); UpdateReplyButtons(); displayAttachmentsForExpandedView(); try { AdjustHeaderView(Services.prefs.getIntPref("mail.show_headers")); } catch (e) { logException(e); } } /** * Ensure that the name columns in both grids are the same size, since the only * reason that we're using two grids at all is to workaround the XUL box * model's inability to float elements. */ function syncGridColumnWidths() { let nameColumn = document.getElementById("expandedHeadersNameColumn"); let nameColumn2 = document.getElementById("expandedHeaders2NameColumn"); // Reset the minimum widths to 0 so that clientWidth will return the // preferred intrinsic width of each column. nameColumn.minWidth = nameColumn2.minWidth = 0; // Set minWidth on the smaller of the two columns to be the width of the // larger of the two. if (nameColumn.clientWidth > nameColumn2.clientWidth) { nameColumn2.minWidth = nameColumn.clientWidth; } else if (nameColumn.clientWidth < nameColumn2.clientWidth) { nameColumn.minWidth = nameColumn2.clientWidth; } } /** * Default method for updating a header value into a header entry * * @param aHeaderEntry A single header from currentHeaderData * @param aHeaderValue The new value for headerEntry */ function updateHeaderValue(aHeaderEntry, aHeaderValue) { aHeaderEntry.enclosingBox.headerValue = aHeaderValue; } /** * Create the DOM nodes (aka "View") for a non-standard header and insert them * into the grid. Create and return the corresponding headerEntry object. * * @param {String} headerName name of the header we're adding, all lower-case; * used to construct element ids * @param {String} label name of the header as displayed in the UI */ function HeaderView(headerName, label) { let rowId = "expanded" + headerName + "Row"; let idName = "expanded" + headerName + "Box"; let newHeaderNode; // If a row for this header already exists, do not create another one. let newRowNode = document.getElementById(rowId); if (!newRowNode) { // Create new collapsed row. newRowNode = document.createElement("row"); newRowNode.setAttribute("id", rowId); newRowNode.collapsed = true; // Create and append the label which contains the header name. let newLabelNode = document.createElement("label"); newLabelNode.setAttribute("id", "expanded" + headerName + "Label"); newLabelNode.setAttribute("value", label); newLabelNode.setAttribute("class", "headerName"); newLabelNode.setAttribute("control", idName); newRowNode.appendChild(newLabelNode); // Create and append the new header value. newHeaderNode = document.createElement("mail-headerfield"); newHeaderNode.setAttribute("id", idName); newHeaderNode.setAttribute("flex", "1"); newRowNode.appendChild(newHeaderNode); // This new element needs to be inserted into the view... let topViewNode = document.getElementById("expandedHeader2Rows"); topViewNode.appendChild(newRowNode); this.isNewHeader = true; } else { newHeaderNode = document.getElementById(idName); this.isNewHeader = false; } this.enclosingBox = newHeaderNode; this.enclosingRow = newRowNode; this.valid = false; this.useToggle = false; this.outputFunction = updateHeaderValue; } /** * Removes all non-predefined header nodes from the view. * * @param aHeaderTable Table of header entries. */ function RemoveNewHeaderViews(aHeaderTable) { for (let name in aHeaderTable) { let headerEntry = aHeaderTable[name]; if (headerEntry.isNewHeader) headerEntry.enclosingRow.remove(); } } /** * UpdateExpandedMessageHeaders: Iterate through all the current header data * we received from mime for this message for the expanded header entry table, * and see if we have a corresponding entry for that header (i.e. * whether the expanded header view cares about this header value) * If so, then call updateHeaderEntry */ function UpdateExpandedMessageHeaders() { // Iterate over each header we received and see if we have a matching entry // in each header view table... var headerName; // Remove the height attr so that it redraws correctly. Works around a problem // that attachment-splitter causes if it's moved high enough to affect // the header box: document.getElementById("msgHeaderView").removeAttribute("height"); // This height attribute may be set by toggleWrap() if the user clicked // the "more" button" in the header. // Remove it so that the height is determined automatically. document.getElementById("expandedHeaderView").removeAttribute("height"); for (headerName in currentHeaderData) { var headerField = currentHeaderData[headerName]; var headerEntry = null; if (headerName in gExpandedHeaderView) headerEntry = gExpandedHeaderView[headerName]; if (!headerEntry && gViewAllHeaders) { // for view all headers, if we don't have a header field for this // value....cheat and create one....then fill in a headerEntry if (headerName == "message-id" || headerName == "in-reply-to") { var messageIdEntry = { name: headerName, outputFunction: OutputMessageIds }; gExpandedHeaderView[headerName] = new createHeaderEntry("expanded", messageIdEntry); } // Don't bother showing X-Mozilla-LocalizedDate, since that value is // displayed below the message header toolbar. else if (headerName != "x-mozilla-localizeddate") { gExpandedHeaderView[headerName] = new HeaderView(headerName, currentHeaderData[headerName].headerName); } headerEntry = gExpandedHeaderView[headerName]; } if (headerEntry) { if (headerName == "references" && !(gViewAllHeaders || gHeadersShowReferences || gFolderDisplay.view.isNewsFolder)) { // Hide references header if view all headers mode isn't selected, the // pref show references is deactivated and the currently displayed // message isn't a newsgroup posting. headerEntry.valid = false; } else { headerEntry.outputFunction(headerEntry, headerField.headerValue); headerEntry.valid = true; } } } let dateLabel = document.getElementById("dateLabel"); if ("x-mozilla-localizeddate" in currentHeaderData) { document.getElementById("dateLabel").textContent = currentHeaderData["x-mozilla-localizeddate"].headerValue; dateLabel.collapsed = false; } else { dateLabel.collapsed = true; } gBuiltExpandedView = true; // Now update the view to make sure the right elements are visible. updateExpandedView(); } function ClearCurrentHeaders() { currentHeaderData = {}; currentAttachments = new Array(); } function ShowMessageHeaderPane() { document.getElementById("msgHeaderView").collapsed = false; } function HideMessageHeaderPane() { document.getElementById("msgHeaderView").collapsed = true; // Disable the Message/Attachments menuitem. document.getElementById("msgAttachmentMenu").setAttribute("disabled", "true"); // If the App Menu is being used, disable the attachment menu in there as // well. let appMenuNode = document.getElementById("appmenu_msgAttachmentMenu"); if (appMenuNode) appMenuNode.setAttribute("disabled", "true"); // disable the attachment box document.getElementById("attachmentView").collapsed = true; document.getElementById("attachment-splitter").collapsed = true; gMessageNotificationBar.clearMsgNotifications(); } /** * Take string of newsgroups separated by commas, split it * into newsgroups and send them to the corresponding * mail-newsgroups-headerfield element. * * @param headerEntry the entry data structure for this header * @param headerValue the string value for the header from the message */ function OutputNewsgroups(headerEntry, headerValue) { headerValue.split(",").forEach( newsgroup => headerEntry.enclosingBox.addNewsgroupView(newsgroup)); headerEntry.enclosingBox.buildViews(); } /** * Take string of message-ids separated by whitespace, split it * into message-ids and send them together with the index number * to the corresponding mail-messageids-headerfield element. */ function OutputMessageIds(headerEntry, headerValue) { let messageIdArray = headerValue.split(/\s+/); headerEntry.enclosingBox.clearHeaderValues(); for (let i = 0; i < messageIdArray.length; i++) headerEntry.enclosingBox.addMessageIdView(messageIdArray[i]); headerEntry.enclosingBox.fillMessageIdNodes(); } /** * OutputEmailAddresses: knows how to take a comma separated list of email * addresses, extracts them one by one, linkifying each email address into * a mailto url. Then we add the link-ified email address to the parentDiv * passed in. * * @param headerEntry parent div * @param emailAddresses comma separated list of the addresses for this * header field */ function OutputEmailAddresses(headerEntry, emailAddresses) { if (!emailAddresses) return; // The email addresses are still RFC2047 encoded but libmime has already converted from // "raw UTF-8" to "wide" (UTF-16) characters. var addresses = MailServices.headerParser.parseEncodedHeaderW(emailAddresses); if (headerEntry.useToggle) headerEntry.enclosingBox.resetAddressView(); // make sure we start clean if (addresses.length == 0 && emailAddresses.includes(":")) { // No addresses and a colon, so an empty group like "undisclosed-recipients: ;". // Add group name so at least something displays. let address = { displayName: emailAddresses }; if (headerEntry.useToggle) headerEntry.enclosingBox.addAddressView(address); else updateEmailAddressNode(headerEntry.enclosingBox.emailAddressNode, address); } for (let addr of addresses) { // If we want to include short/long toggle views and we have a long view, // always add it. If we aren't including a short/long view OR if we are and // we haven't parsed enough addresses to reach the cutoff valve yet then add // it to the default (short) div. let address = {}; address.emailAddress = addr.email; address.fullAddress = addr.toString(); address.displayName = addr.name; if (headerEntry.useToggle) headerEntry.enclosingBox.addAddressView(address); else updateEmailAddressNode(headerEntry.enclosingBox.emailAddressNode, address); } if (headerEntry.useToggle) headerEntry.enclosingBox.buildViews(); } function updateEmailAddressNode(emailAddressNode, address) { emailAddressNode.setAttribute("emailAddress", address.emailAddress || ""); emailAddressNode.setAttribute("fullAddress", address.fullAddress || ""); emailAddressNode.setAttribute("displayName", address.displayName || ""); if (address.emailAddress) UpdateEmailNodeDetails(address.emailAddress, emailAddressNode); } function UpdateEmailNodeDetails(aEmailAddress, aDocumentNode, aCardDetails) { // If we haven't been given specific details, search for a card. var cardDetails = aCardDetails ? aCardDetails : getCardForEmail(aEmailAddress); aDocumentNode.cardDetails = cardDetails; if (!cardDetails.card) { aDocumentNode.setAttribute("hascard", "false"); aDocumentNode.setAttribute("tooltipstar", document.getElementById("addToAddressBookItem").label); } else { aDocumentNode.setAttribute("hascard", "true"); aDocumentNode.setAttribute("tooltipstar", document.getElementById("editContactItem").label); } // When we are adding cards, we don't want to move the display around if the // user has clicked on the star, therefore if it is locked, just exit and // leave the display updates until later. if (aDocumentNode.hasAttribute("updatingUI")) return; var displayName = FormatDisplayName(aEmailAddress, aDocumentNode.getAttribute("displayName"), aDocumentNode.getAttribute("headerName"), aDocumentNode.cardDetails.card); if (gShowCondensedEmailAddresses && displayName) { aDocumentNode.setAttribute("label", displayName); aDocumentNode.setAttribute("tooltiptext", aEmailAddress); } else { aDocumentNode.setAttribute("label", aDocumentNode.getAttribute("fullAddress") || aDocumentNode.getAttribute("displayName")); } } function UpdateEmailPresenceDetails(aDocumentNode, aChatContact) { aDocumentNode.removeAttribute("chatStatus"); aDocumentNode.removeAttribute("presenceTooltip"); } function UpdateExtraAddressProcessing(aAddressData, aDocumentNode, aAction, aParentDir, aItem) { switch (aAction) { case nsIAbListener.itemChanged: if (aAddressData && aDocumentNode.cardDetails.card && aItem.hasEmailAddress(aAddressData.emailAddress)) { aDocumentNode.cardDetails.card = aItem; var displayName = FormatDisplayName(aAddressData.emailAddress, aDocumentNode.getAttribute("displayName"), aDocumentNode.getAttribute("headerName"), aDocumentNode.cardDetails.card); if (gShowCondensedEmailAddresses && displayName) { aDocumentNode.setAttribute("label", displayName); } else { aDocumentNode.setAttribute("label", aDocumentNode.getAttribute("fullAddress") || aDocumentNode.getAttribute("displayName")); } } break; case nsIAbListener.itemAdded: // Is it a new address book? if (aItem instanceof nsIAbDirectory) { // If we don't have a match, search again for updates (e.g. a interface // to an existing book may just have been added). if (!aDocumentNode.cardDetails.card) UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode); } else if (aItem instanceof nsIAbCard) { // If we don't have a card, does this new one match? if (aDocumentNode.cardDetails && !aDocumentNode.cardDetails.card && aItem.hasEmailAddress(aAddressData.emailAddress)) { // Just in case we have a bogus parent directory. if (aParentDir instanceof nsIAbDirectory) { var cardDetails = { book: aParentDir, card: aItem }; UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode, cardDetails); } else { UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode); } } } break; case nsIAbListener.directoryItemRemoved: // Unfortunately we don't necessarily get the same card object back. if (aAddressData && aDocumentNode.cardDetails && aDocumentNode.cardDetails.card && aDocumentNode.cardDetails.book == aParentDir && aItem.hasEmailAddress(aAddressData.emailAddress)) { UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode); } break; case nsIAbListener.directoryRemoved: if (aDocumentNode.cardDetails.book == aItem) UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode); break; } } function findEmailNodeFromPopupNode(elt, popup) { // This annoying little function is needed because in the binding for // mail-emailaddress, we set the context on the , but that if // the user clicks on the label, then popupNode is set to it, rather than // the description. So we have walk up the parent until we find the // element with the popup set, and then return its parent. while (elt.getAttribute("popup") != popup) { elt = elt.parentNode; if (elt == null) return null; } return elt.parentNode; } function hideEmailNewsPopup(addressNode) { // highlight the emailBox/newsgroupBox addressNode.removeAttribute("selected"); } function setupEmailAddressPopup(emailAddressNode) { var emailAddressPlaceHolder = document.getElementById("emailAddressPlaceHolder"); var emailAddress = emailAddressNode.getPart("emaillabel").value; emailAddressNode.setAttribute("selected", "true"); emailAddressPlaceHolder.setAttribute("label", emailAddress); if (emailAddressNode.cardDetails && emailAddressNode.cardDetails.card) { document.getElementById("addToAddressBookItem").setAttribute("hidden", true); if (!emailAddressNode.cardDetails.book.readOnly) { document.getElementById("editContactItem").removeAttribute("hidden"); document.getElementById("viewContactItem").setAttribute("hidden", true); } else { document.getElementById("editContactItem").setAttribute("hidden", true); document.getElementById("viewContactItem").removeAttribute("hidden"); } } else { document.getElementById("addToAddressBookItem").removeAttribute("hidden"); document.getElementById("editContactItem").setAttribute("hidden", true); document.getElementById("viewContactItem").setAttribute("hidden", true); } } /** * Returns an object with two properties, book and card. If the email address * is found in the address books, then the book will contain an nsIAbDirectory, * and card will contain an nsIAbCard. If the email address is not found, both * items will contain null. * * @param emailAddress address to look for * @return an object with two properties, .book and .card */ function getCardForEmail(emailAddress) { // Email address is searched for in any of the address books that support // the cardForEmailAddress function. // Future expansion could be to domain matches var books = MailServices.ab.directories; var result = { book: null, card: null }; while (!result.card && books.hasMoreElements()) { var ab = books.getNext().QueryInterface(nsIAbDirectory); try { var card = ab.cardForEmailAddress(emailAddress); if (card) { result.book = ab; result.card = card; } } catch (ex) { } } return result; } function onClickEmailStar(event, emailAddressNode) { // Only care about left-click events if (event.button != 0) return; if (emailAddressNode && emailAddressNode.cardDetails && emailAddressNode.cardDetails.card) { EditContact(emailAddressNode); } else { AddContact(emailAddressNode); } } function onClickEmailPresence(event, emailAddressNode) { // Only care about left-click events if (event.button != 0) return; let prplConv = emailAddressNode.chatContact.createConversation(); let uiConv = Services.conversations.getUIConversation(prplConv); let win = window; if (!("focusConversation" in chatHandler)) { win = Services.wm.getMostRecentWindow("mail:3pane"); if (win) win.focus(); else { window.openDialog("chrome://messenger/content/", "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar", null, {tabType: "chat", tabParams: {convType: "focus", conv: uiConv}}); return; } } win.showChatTab(); win.chatHandler.focusConversation(uiConv); } /** * Takes the email address node, adds a new contact from the node's * displayName and emailAddress attributes to the personal address book. * * @param emailAddressNode a node with displayName and emailAddress attributes */ function AddContact(emailAddressNode) { // When we collect an address, it updates the AB which sends out // notifications to update the UI. In the add case we don't want to update // the UI so that accidentally double-clicking on the star doesn't lead // to something strange (i.e star would be moved out from underneath, // leaving something else there). emailAddressNode.setAttribute("updatingUI", true); const kPersonalAddressbookURI = "moz-abmdbdirectory://abook.mab"; let addressBook = MailServices.ab.getDirectory(kPersonalAddressbookURI); let card = Components.classes["@mozilla.org/addressbook/cardproperty;1"] .createInstance(Components.interfaces.nsIAbCard); card.displayName = emailAddressNode.getAttribute("displayName"); card.primaryEmail = emailAddressNode.getAttribute("emailAddress"); // Just save the new node straight away. addressBook.addCard(card); emailAddressNode.removeAttribute("updatingUI"); } function EditContact(emailAddressNode) { if (emailAddressNode.cardDetails.card) editContactInlineUI.showEditContactPanel(emailAddressNode.cardDetails, emailAddressNode); } /** * Takes the email address title button, extracts the email address we stored * in there and opens a compose window with that address. * * @param addressNode a node which has a "fullAddress" or "newsgroup" attribute * @param aEvent the event object when user triggers the menuitem */ function SendMailToNode(addressNode, aEvent) { let fields = Components.classes["@mozilla.org/messengercompose/composefields;1"] .createInstance(Components.interfaces.nsIMsgCompFields); let params = Components.classes["@mozilla.org/messengercompose/composeparams;1"] .createInstance(Components.interfaces.nsIMsgComposeParams); fields.newsgroups = addressNode.getAttribute("newsgroup"); if (addressNode.hasAttribute("fullAddress")) { let addresses = MailServices.headerParser.makeFromDisplayAddress( addressNode.getAttribute("fullAddress"), {}); if (addresses.length > 0) fields.to = MailServices.headerParser.makeMimeHeader(addresses, 1); } params.type = Components.interfaces.nsIMsgCompType.New; // If aEvent is passed, check if Shift key was pressed for composition in // non-default format (HTML vs. plaintext). params.format = (aEvent && aEvent.shiftKey) ? Components.interfaces.nsIMsgCompFormat.OppositeOfDefault : Components.interfaces.nsIMsgCompFormat.Default; if (gFolderDisplay.displayedFolder) { params.identity = accountManager.getFirstIdentityForServer( gFolderDisplay.displayedFolder.server); } params.composeFields = fields; MailServices.compose.OpenComposeWindowWithParams(null, params); } /** * Takes the email address or newsgroup title button, extracts the address/name * we stored in there and copies it to the clipboard. * * @param addressNode a node which has an "emailAddress" or "newsgroup" * attribute * @param aIncludeName when true, also copy the name onto the clipboard, * otherwise only the email address */ function CopyEmailNewsAddress(addressNode, aIncludeName = false) { let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"] .getService(Components.interfaces.nsIClipboardHelper); let address = addressNode.getAttribute(aIncludeName ? "fullAddress" : "emailAddress") || addressNode.getAttribute("newsgroup"); clipboard.copyString(address); } /** * Causes the filter dialog to pop up, prefilled for the specified e-mail * address or header value. * * @param aHeaderNode A node which has an "emailAddress" attribute * or a "headerName" attribute. * @param aMessage Optional nsIMsgHdr of the message from which the values * are taken. Will be used to preselect its folder in the * filter list. */ function CreateFilter(aHeaderNode, aMessage) { let nodeIsAddress = aHeaderNode.hasAttribute("emailAddress"); let nodeValue = nodeIsAddress ? aHeaderNode.getAttribute("emailAddress") : document.getAnonymousNodes(aHeaderNode)[0].textContent; let folder = aMessage ? aMessage.folder : null; top.MsgFilters(nodeValue, folder, aHeaderNode.getAttribute("headerName")); } /** * Get the newsgroup server corresponding to the currently selected message. * * @return nsISubscribableServer for the newsgroup, or null */ function GetNewsgroupServer() { if (gFolderDisplay.selectedMessageIsNews) { let server = gFolderDisplay.selectedMessage.folder.server; if (server) return server.QueryInterface(Components.interfaces.nsISubscribableServer); } return null; } /** * Initialize the newsgroup popup, showing/hiding menu items as appropriate. * * @param newsgroupNode a node which has a "newsgroup" attribute */ function setupNewsgroupPopup(newsgroupNode) { let newsgroupPlaceHolder = document.getElementById("newsgroupPlaceHolder"); let newsgroup = newsgroupNode.getAttribute("newsgroup"); newsgroupNode.setAttribute("selected", "true"); newsgroupPlaceHolder.setAttribute("label", newsgroup); let server = GetNewsgroupServer(); if (server) { // XXX Why is this necessary when nsISubscribableServer contains // |isSubscribed|? server = server.QueryInterface(Components.interfaces.nsINntpIncomingServer); if (!server.containsNewsgroup(newsgroup)) { document.getElementById("subscribeToNewsgroupItem") .removeAttribute("hidden"); document.getElementById("subscribeToNewsgroupSeparator") .removeAttribute("hidden"); return; } } document.getElementById("subscribeToNewsgroupItem") .setAttribute("hidden", true); document.getElementById("subscribeToNewsgroupSeparator") .setAttribute("hidden", true); } /** * Subscribe to a newsgroup based on the newsgroup title button * * @param newsgroupNode a node which has a "newsgroup" attribute */ function SubscribeToNewsgroup(newsgroupNode) { let server = GetNewsgroupServer(); if (server) { let newsgroup = newsgroupNode.getAttribute("newsgroup"); server.subscribe(newsgroup); server.commitSubscribeChanges(); } } /** * Takes the newsgroup address title button, extracts the newsgroup name we * stored in there and copies it to the clipboard. * * @param newsgroupNode a node which has a "newsgroup" attribute */ function CopyNewsgroupName(newsgroupNode) { let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"] .getService(Components.interfaces.nsIClipboardHelper); clipboard.copyString(newsgroupNode.getAttribute("newsgroup")); } /** * Takes the newsgroup address title button, extracts the newsgroup name we * stored in there and copies it URL to it. * * @param newsgroupNode a node which has a "newsgroup" attribute */ function CopyNewsgroupURL(newsgroupNode) { let server = GetNewsgroupServer(); if (!server) return; let ng = newsgroupNode.getAttribute("newsgroup"); let url; if (server.socketType != Components.interfaces.nsMsgSocketType.SSL) { url = "news://" + server.hostName; if (server.port != Components.interfaces.nsINntpUrl.DEFAULT_NNTP_PORT) url += ":" + server.port; url += "/" + ng; } else { url = "snews://" + server.hostName; if (server.port != Components.interfaces.nsINntpUrl.DEFAULT_NNTPS_PORT) url += ":" + server.port; url += "/" + ng; } try { let uri = Services.io.newURI(url, null, null); let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"] .getService(Components.interfaces.nsIClipboardHelper); clipboard.copyString(decodeURI(uri.spec)); } catch(e) { Components.utils.reportError("Invalid URL: "+ url); } } /** * Create a new attachment object which goes into the data attachment array. * This method checks whether the passed attachment is empty or not. * * @param contentType The attachment's mimetype * @param url The URL for the attachment * @param name The name to be displayed for this attachment (usually the * filename) * @param uri The URI for the message containing the attachment * @param isExternalAttachment True if the attachment has been detached * @param size The size in bytes of the attachment */ function AttachmentInfo(contentType, url, name, uri, isExternalAttachment, size) { this.contentType = contentType; this.name = name; this.uri = uri; this.isExternalAttachment = isExternalAttachment; this.size = size; let match; // Remove [?&]part= from remote urls, after getting the partID. // Remote urls, unlike non external mail part urls, may also contain query // strings starting with ?; PART_RE does not handle this. if (url.startsWith("http") || url.startsWith("file")) { match = url.match(/[?&]part=[^&]+$/); match = match && match[0]; this.partID = match && match.split("part=")[1]; url = url.replace(match, ""); } else { match = GlodaUtils.PART_RE.exec(url); this.partID = match && match[1]; } this.url = url; } AttachmentInfo.prototype = { /** * Save this attachment to a file. */ save: function AttachmentInfo_save() { messenger.saveAttachment(this.contentType, this.url, encodeURIComponent(this.name), this.uri, this.isExternalAttachment); }, /** * Open this attachment. */ open: function AttachmentInfo_open() { if (!this.hasFile) return; if (this.isEmpty) { var prompt = document.getElementById("bundle_messenger") .getString("emptyAttachment"); msgWindow.promptDialog.alert(null, prompt); } else { messenger.openAttachment(this.contentType, this.url, encodeURIComponent(this.name), this.uri, this.isExternalAttachment); } }, /** * Detach this attachment from the message. * * @param aSaveFirst true if the attachment should be saved before detaching, * false otherwise */ detach: function AttachmentInfo_detach(aSaveFirst) { messenger.detachAttachment(this.contentType, this.url, encodeURIComponent(this.name), this.uri, aSaveFirst); }, /** * This method checks whether the attachment has been deleted or not. * * @return true if the attachment has been deleted, false otherwise */ get isDeleted() { return this.contentType == "text/x-moz-deleted"; }, /** * This method checks whether the attachment has an associated file or not. * Deleted attachments or detached attachments with missing external files * do *not* have a file. * * @return true if the attachment has an associated file, false otherwise */ get hasFile() { if (this.isDeleted) return false; if (this.isExternalAttachment && this.url.startsWith("file:") && this.size === null) return false; return true; }, /** * This method checks whether the attachment is empty or not. * * @return true if the attachment is empty, false otherwise */ get isEmpty() { // Create an input stream on the attachment url. let url = Services.io.newURI(this.url, null, null); let channel = Services.io.newChannelFromURI2(url, null, Services.scriptSecurityManager.getSystemPrincipal(), null, Components.interfaces.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, Components.interfaces.nsIContentPolicy.TYPE_OTHER); let stream = channel.open(); let inputStream = Components.classes["@mozilla.org/binaryinputstream;1"] .createInstance(Components.interfaces.nsIBinaryInputStream); inputStream.setInputStream(stream); let bytesAvailable = 0; if (inputStream.isNonBlocking()) { // If the stream does not block, test on two conditions: // - attachment is empty -> 0 bytes will be returned on readBytes() // - attachment is not empty -> NS_BASE_STREAM_WOULD_BLOCK exception is // thrown let chunk = null; try { chunk = inputStream.readBytes(1); } catch (ex) { if (ex.result == Components.results.NS_BASE_STREAM_WOULD_BLOCK) { bytesAvailable = 1; } else { throw ex; } } if (chunk) bytesAvailable = chunk.length; } else { // If the stream blocks, we can rely on available() to return the correct // number. bytesAvailable = inputStream.available(); } return (bytesAvailable == 0); }, }; /** * Return true if possible attachments in the currently loaded message can be * deleted/detached. */ function CanDetachAttachments() { var canDetach = !gFolderDisplay.selectedMessageIsNews && (!gFolderDisplay.selectedMessageIsImap || MailOfflineMgr.isOnline()); if (canDetach && ("content-type" in currentHeaderData)) canDetach = !ContentTypeIsSMIME(currentHeaderData["content-type"].headerValue); return canDetach; } /** * Return true if the content type is an S/MIME one. */ function ContentTypeIsSMIME(contentType) { // S/MIME is application/pkcs7-mime and application/pkcs7-signature // - also match application/x-pkcs7-mime and application/x-pkcs7-signature. return /application\/(x-)?pkcs7-(mime|signature)/.test(contentType); } function onShowAttachmentToolbarContextMenu() { let expandBar = document.getElementById("context-expandAttachmentBar"); let expanded = Services.prefs.getBoolPref( "mailnews.attachments.display.start_expanded"); expandBar.setAttribute("checked", expanded); } /** * Set up the attachment item context menu, showing or hiding the appropriate * menu items. */ function onShowAttachmentItemContextMenu() { let attachmentList = document.getElementById("attachmentList"); let attachmentInfo = document.getElementById("attachmentInfo"); let attachmentName = document.getElementById("attachmentName"); let contextMenu = document.getElementById("attachmentItemContext"); let openMenu = document.getElementById("context-openAttachment"); let saveMenu = document.getElementById("context-saveAttachment"); let detachMenu = document.getElementById("context-detachAttachment"); let deleteMenu = document.getElementById("context-deleteAttachment"); let copyUrlMenuSep = document.getElementById("context-menu-copyurl-separator"); let copyUrlMenu = document.getElementById("context-copyAttachmentUrl"); // If we opened the context menu from the attachment info area (the paperclip, // "1 attachment" label, filename, or file size, just grab the first (and // only) attachment as our "selected" attachments. var selectedAttachments; if (contextMenu.triggerNode == attachmentInfo || contextMenu.triggerNode.parentNode == attachmentInfo) { selectedAttachments = [attachmentList.getItemAtIndex(0).attachment]; if (contextMenu.triggerNode == attachmentName) attachmentName.setAttribute("selected", true); } else { selectedAttachments = [...attachmentList.selectedItems].map(item => item.attachment); } contextMenu.attachments = selectedAttachments; var allSelectedDetached = selectedAttachments.every(function(attachment) { return attachment.isExternalAttachment; }); var allSelectedDeleted = selectedAttachments.every(function(attachment) { return !attachment.hasFile; }); var canDetachSelected = CanDetachAttachments() && !allSelectedDetached && !allSelectedDeleted; let allSelectedHttp = selectedAttachments.every(function(attachment) { return attachment.url.startsWith("http"); }); openMenu.disabled = allSelectedDeleted; saveMenu.disabled = allSelectedDeleted; detachMenu.disabled = !canDetachSelected; deleteMenu.disabled = !canDetachSelected; copyUrlMenuSep.hidden = copyUrlMenu.hidden = !allSelectedHttp; } /** * Close the attachment item context menu, performing any cleanup as necessary. */ function onHideAttachmentItemContextMenu() { let attachmentName = document.getElementById("attachmentName"); let contextMenu = document.getElementById("attachmentItemContext"); // If we opened the context menu from the attachmentName label, we need to // get rid of the "selected" attribute. if (contextMenu.triggerNode == attachmentName) attachmentName.removeAttribute("selected"); } /** * Enable/disable menu items as appropriate for the single-attachment save all * toolbar button. */ function onShowSaveAttachmentMenuSingle() { let openItem = document.getElementById('button-openAttachment'); let saveItem = document.getElementById('button-saveAttachment'); let detachItem = document.getElementById('button-detachAttachment'); let deleteItem = document.getElementById('button-deleteAttachment'); let detached = currentAttachments[0].isExternalAttachment; let deleted = !currentAttachments[0].hasFile; let canDetach = CanDetachAttachments() && !deleted && !detached; openItem.disabled = deleted; saveItem.disabled = deleted; detachItem.disabled = !canDetach; deleteItem.disabled = !canDetach; } /** * Enable/disable menu items as appropriate for the multiple-attachment save all * toolbar button. */ function onShowSaveAttachmentMenuMultiple() { let openAllItem = document.getElementById('button-openAllAttachments'); let saveAllItem = document.getElementById('button-saveAllAttachments'); let detachAllItem = document.getElementById('button-detachAllAttachments'); let deleteAllItem = document.getElementById('button-deleteAllAttachments'); let allDetached = currentAttachments.every(function(attachment) { return attachment.isExternalAttachment; }); let allDeleted = currentAttachments.every(function(attachment) { return !attachment.hasFile; }); let canDetach = CanDetachAttachments() && !allDeleted && !allDetached; openAllItem.disabled = allDeleted; saveAllItem.disabled = allDeleted; detachAllItem.disabled = !canDetach; deleteAllItem.disabled = !canDetach; } function MessageIdClick(node, event) { if (event.button == 0) { var messageId = GetMessageIdFromNode(node, true); OpenMessageForMessageId(messageId); } } /** * This is our oncommand handler for the attachment list items. A double click * or enter press in an attachmentitem simulates "opening" the attachment. * * @param event the event object */ function attachmentItemCommand(event) { HandleSelectedAttachments("open"); } var AttachmentListController = { supportsCommand: function(command) { switch (command) { case "cmd_selectAll": case "cmd_delete": case "cmd_shiftDelete": case "cmd_saveAsFile": return true; default: return false; } }, isCommandEnabled: function(command) { switch (command) { case "cmd_selectAll": case "cmd_delete": case "cmd_shiftDelete": case "cmd_saveAsFile": return true; default: return false; } }, doCommand: function(command) { // If the user invoked a key short cut then it is possible that we got here // for a command which is really disabled. kick out if the command should // be disabled. if (!this.isCommandEnabled(command)) return; var attachmentList = document.getElementById("attachmentList"); switch (command) { case "cmd_selectAll": attachmentList.selectAll(); return; case "cmd_delete": case "cmd_shiftDelete": HandleSelectedAttachments("delete"); return; case "cmd_saveAsFile": HandleSelectedAttachments("saveAs"); return; } }, onEvent: function(event) {} }; var AttachmentMenuController = { commands: { cmd_openAllAttachments: { isEnabled: function() { return AttachmentMenuController._someFilesAvailable(); }, doCommand: function() { HandleAllAttachments("open"); }, }, cmd_saveAllAttachments: { isEnabled: function() { return AttachmentMenuController._someFilesAvailable(); }, doCommand: function() { HandleAllAttachments("save"); }, }, cmd_detachAllAttachments: { isEnabled: function() { return AttachmentMenuController._canDetachFiles(); }, doCommand: function() { HandleAllAttachments("detach"); }, }, cmd_deleteAllAttachments: { isEnabled: function() { return AttachmentMenuController._canDetachFiles(); }, doCommand: function() { HandleAllAttachments("delete"); }, }, }, _canDetachFiles: function() { let someNotDetached = currentAttachments.some(function(aAttachment) { return !aAttachment.isExternalAttachment; }); return CanDetachAttachments() && someNotDetached && this._someFilesAvailable(); }, _someFilesAvailable: function() { return currentAttachments.some(function(aAttachment) { return aAttachment.hasFile; }); }, supportsCommand: function(aCommand) { return (aCommand in this.commands); }, isCommandEnabled: function(aCommand) { if (!this.supportsCommand(aCommand)) return false; return this.commands[aCommand].isEnabled(); }, doCommand: function(aCommand) { if (!this.supportsCommand(aCommand)) return; let cmd = this.commands[aCommand]; if (!cmd.isEnabled()) return; cmd.doCommand(); }, onEvent: function(aEvent) {} }; function goUpdateAttachmentCommands() { goUpdateCommand('cmd_openAllAttachments'); goUpdateCommand('cmd_saveAllAttachments'); goUpdateCommand('cmd_detachAllAttachments'); goUpdateCommand('cmd_deleteAllAttachments'); } function displayAttachmentsForExpandedView() { var bundle = document.getElementById("bundle_messenger"); var numAttachments = currentAttachments.length; var totalSize = 0; var attachmentView = document.getElementById("attachmentView"); var attachmentSplitter = document.getElementById("attachment-splitter"); if (numAttachments <= 0) { attachmentView.collapsed = true; attachmentSplitter.collapsed = true; } else if (!gBuildAttachmentsForCurrentMsg) { attachmentView.collapsed = false; var attachmentList = document.getElementById("attachmentList"); var viewMode = Services.prefs.getIntPref("mailnews.attachments.display.view"); var views = ["small", "large", "tile"]; attachmentList.view = views[viewMode]; attachmentList.controllers.appendController(AttachmentListController); toggleAttachmentList(false); var lastPartID; var unknownSize = false; for (let attachment of currentAttachments) { // Create a new attachment widget var displayName = SanitizeAttachmentDisplayName(attachment); var item = attachmentList.appendItem(attachment, displayName); item.setAttribute("tooltiptext", attachment.name); item.addEventListener("command", attachmentItemCommand, false); // Check if this attachment's part ID is a child of the last attachment // we counted. If so, skip it, since we already accounted for its size // from its parent. if (!lastPartID || attachment.partID.indexOf(lastPartID) != 0) { lastPartID = attachment.partID; if (attachment.size !== null) totalSize += attachment.size; else if (!attachment.isDeleted) unknownSize = true; } } // Show the appropriate toolbar button and label based on the number of // attachments. updateSaveAllAttachmentsButton(); let attachmentInfo = document.getElementById("attachmentInfo"); let attachmentCount = document.getElementById("attachmentCount"); let attachmentName = document.getElementById("attachmentName"); let attachmentSize = document.getElementById("attachmentSize"); if (numAttachments == 1) { let count = bundle.getString("attachmentCountSingle"); let name = SanitizeAttachmentDisplayName(currentAttachments[0]); attachmentInfo.setAttribute("contextmenu", "attachmentItemContext"); attachmentCount.setAttribute("value", count); attachmentName.hidden = false; attachmentName.setAttribute("value", name); } else { let words = bundle.getString("attachmentCount"); let count = PluralForm.get(currentAttachments.length, words) .replace("#1", currentAttachments.length); attachmentInfo.setAttribute("contextmenu", "attachmentListContext"); attachmentCount.setAttribute("value", count); attachmentName.hidden = true; } let sizeStr = messenger.formatFileSize(totalSize); if (unknownSize) { if (totalSize == 0) sizeStr = bundle.getString("attachmentSizeUnknown"); else sizeStr = bundle.getFormattedString("attachmentSizeAtLeast", [sizeStr]); } attachmentSize.setAttribute("value", sizeStr); gBuildAttachmentsForCurrentMsg = true; } } /** * Update the "save all attachments" button in the attachment pane, showing * the proper button and enabling/disabling it as appropriate. */ function updateSaveAllAttachmentsButton() { let saveAllSingle = document.getElementById("attachmentSaveAllSingle"); let saveAllMultiple = document.getElementById("attachmentSaveAllMultiple"); // If we can't find the buttons, they're not on the toolbar, so bail out! if (!saveAllSingle || !saveAllMultiple) return; let allDeleted = currentAttachments.every(function(attachment) { return !attachment.hasFile; }); let single = (currentAttachments.length == 1); saveAllSingle.hidden = !single; saveAllMultiple.hidden = single; saveAllSingle.disabled = saveAllMultiple.disabled = allDeleted; } /** * Expand/collapse the attachment list. When expanding it, automatically resize * it to an appropriate height (1/4 the message pane or smaller). * * @param expanded True if the attachment list should be expanded, false * otherwise. If |expanded| is not specified, toggle the state. * @param updateFocus (optional) True if the focus should be updated, focusing * on the attachmentList when expanding, or the messagepane * when collapsing (but only when the attachmentList was * originally focused). */ function toggleAttachmentList(expanded, updateFocus) { var attachmentView = document.getElementById("attachmentView"); var attachmentBar = document.getElementById("attachmentBar"); var attachmentToggle = document.getElementById("attachmentToggle"); var attachmentList = document.getElementById("attachmentList"); var attachmentSplitter = document.getElementById("attachment-splitter"); var bundle = document.getElementById("bundle_messenger"); if (expanded === undefined) expanded = !attachmentToggle.checked; attachmentToggle.checked = expanded; if (expanded) { attachmentList.collapsed = false; if (!attachmentView.collapsed) attachmentSplitter.collapsed = false; attachmentBar.setAttribute("tooltiptext", bundle.getString( "collapseAttachmentPaneTooltip")); attachmentList.setOptimumWidth(); var attachmentHeight = attachmentView.boxObject.height - attachmentList.boxObject.height + attachmentList.preferredHeight; // If the attachments box takes up too much of the message pane, downsize: var maxAttachmentHeight = document.getElementById("messagepanebox") .boxObject.height / 4; attachmentView.setAttribute("height", Math.min(attachmentHeight, maxAttachmentHeight)); attachmentView.setAttribute("maxheight", attachmentHeight); if (updateFocus) attachmentList.focus(); } else { attachmentList.collapsed = true; attachmentSplitter.collapsed = true; attachmentBar.setAttribute("tooltiptext", bundle.getString( "expandAttachmentPaneTooltip")); attachmentView.removeAttribute("height"); attachmentView.removeAttribute("maxheight"); if (updateFocus && document.activeElement == attachmentList) SetFocusMessagePane(); } } /** * Pick out a nice icon for the attachment. * @param attachment the nsIMsgAttachment object to show icon for */ function getIconForAttachment(attachment) { if (attachment.isDeleted) { return "chrome://messenger/skin/icon/attachment-deleted.png"; } else { return "moz-icon://" + attachment.name + "?size=16&contentType=" + attachment.contentType; } } /** * Public method called when we create the attachments file menu */ function FillAttachmentListPopup(aEvent, aPopup) { // First clear out the old view... ClearAttachmentMenu(aPopup); for (let [attachmentIndex, attachment] of currentAttachments.entries()) addAttachmentToPopup(aPopup, attachment, attachmentIndex); goUpdateAttachmentCommands(); } // Public method used to clear the file attachment menu function ClearAttachmentMenu(popup) { if (popup) { while (popup.firstChild.localName == "menu") popup.firstChild.remove(); } } /** * Create a menu for a single attachment. * * @param popup the popup to add the menu to * @param attachment the AttachmentInfo object to add * @param attachmentIndex the index (starting at 0) of this attachment */ function addAttachmentToPopup(popup, attachment, attachmentIndex) { if (!popup) return; var item = document.createElement("menu"); if (!item) return; function getString(aName) { return document.getElementById("bundle_messenger").getString(aName); } // Insert the item just before the separator. The separator is the 2nd to // last element in the popup. item.setAttribute("class", "menu-iconic"); item.setAttribute("image", getIconForAttachment(attachment)); var numItemsInPopup = popup.childNodes.length; // find the separator var indexOfSeparator = 0; while (popup.childNodes[indexOfSeparator].localName != "menuseparator") indexOfSeparator++; // We increment the attachmentIndex here since we only use it for the // label and accesskey attributes, and we want the accesskeys for the // attachments list in the menu to be 1-indexed. attachmentIndex++; var displayName = SanitizeAttachmentDisplayName(attachment); var label = document.getElementById("bundle_messenger") .getFormattedString("attachmentDisplayNameFormat", [attachmentIndex, displayName]); item.setAttribute("crop", "center"); item.setAttribute("label", label); item.setAttribute("accesskey", attachmentIndex % 10); // Each attachment in the list gets its own menupopup with options for // saving, deleting, detaching, etc. var openpopup = document.createElement("menupopup"); openpopup = item.appendChild(openpopup); openpopup.addEventListener("popupshowing", function(aEvent) { aEvent.stopPropagation(); }); // Due to Bug #314228, we must append our menupopup to the new attachment // menu item before we inserting the attachment menu into the popup. If we // don't, our attachment menu items will not show up. item = popup.insertBefore(item, popup.childNodes[indexOfSeparator]); var detached = attachment.isExternalAttachment; var deleted = !attachment.hasFile; var canDetach = CanDetachAttachments() && !deleted && !detached; if (deleted) { // We can't do anything with a deleted attachment, so just return. item.disabled = true; return; } // Create the "open" menu item var menuitementry = document.createElement("menuitem"); menuitementry.attachment = attachment; menuitementry.setAttribute("oncommand", "this.attachment.open();"); menuitementry.setAttribute("label", getString("openLabel")); menuitementry.setAttribute("accesskey", getString("openLabelAccesskey")); menuitementry.setAttribute("disabled", deleted); menuitementry = openpopup.appendChild(menuitementry); // Create a menuseparator var menuseparator = document.createElement("menuseparator"); openpopup.appendChild(menuseparator); // Create the "save" menu item menuitementry = document.createElement("menuitem"); menuitementry.attachment = attachment; menuitementry.setAttribute("oncommand", "this.attachment.save();"); menuitementry.setAttribute("label", getString("saveLabel")); menuitementry.setAttribute("accesskey", getString("saveLabelAccesskey")); menuitementry.setAttribute("disabled", deleted); menuitementry = openpopup.appendChild(menuitementry); // Create the "detach" menu item menuitementry = document.createElement("menuitem"); menuitementry.attachment = attachment; menuitementry.setAttribute("oncommand", "this.attachment.detach(true);"); menuitementry.setAttribute("label", getString("detachLabel")); menuitementry.setAttribute("accesskey", getString("detachLabelAccesskey")); menuitementry.setAttribute("disabled", !canDetach); menuitementry = openpopup.appendChild(menuitementry); // Create the "delete" menu item menuitementry = document.createElement("menuitem"); menuitementry.attachment = attachment; menuitementry.setAttribute("oncommand", "this.attachment.detach(false);"); menuitementry.setAttribute("label", getString("deleteLabel")); menuitementry.setAttribute("accesskey", getString("deleteLabelAccesskey")); menuitementry.setAttribute("disabled", !canDetach); menuitementry = openpopup.appendChild(menuitementry); } /** * Open an attachment from the attachment bar. * * @param event the event that triggered this action */ function OpenAttachmentFromBar(event) { if (event.button == 0) { // Only open on the first click; ignore double-clicks so that the user // doesn't end up with the attachment opened multiple times. if (event.detail == 1) TryHandleAllAttachments('open'); RestoreFocusAfterHdrButton(); event.stopPropagation(); } } /** * Handle all the attachments in this message (save them, open them, etc). * * @param action one of "open", "save", "saveAs", "detach", or "delete" */ function HandleAllAttachments(action) { HandleMultipleAttachments(currentAttachments, action); } /** * Try to handle all the attachments in this message (save them, open them, * etc). If the action fails for whatever reason, catch the error and report it. * * @param action one of "open", "save", "saveAs", "detach", or "delete" */ function TryHandleAllAttachments(action) { try { HandleAllAttachments(action) } catch (e) { Components.utils.reportError(e); } } /** * Handle the currently-selected attachments in this message (save them, open * them, etc). * * @param action one of "open", "save", "saveAs", "detach", or "delete" */ function HandleSelectedAttachments(action) { let attachmentList = document.getElementById("attachmentList"); let selectedAttachments = []; for (let item of attachmentList.selectedItems) { selectedAttachments.push(item.attachment); } HandleMultipleAttachments(selectedAttachments, action); } /** * Perform an action on multiple attachments (e.g. open or save) * * @param attachments an array of AttachmentInfo objects to work with * @param action one of "open", "save", "saveAs", "detach", or "delete" */ function HandleMultipleAttachments(attachments, action) { // convert our attachment data into some c++ friendly structs var attachmentContentTypeArray = []; var attachmentUrlArray = []; var attachmentDisplayNameArray = []; var attachmentMessageUriArray = []; // populate these arrays.. var actionIndex = 0; for (let attachment of attachments) { // Exclude attachment which are 1) deleted, or 2) detached with missing // external files. if (!attachment.hasFile) continue; attachmentContentTypeArray[actionIndex] = attachment.contentType; attachmentUrlArray[actionIndex] = attachment.url; attachmentDisplayNameArray[actionIndex] = encodeURI(attachment.name); attachmentMessageUriArray[actionIndex] = attachment.uri; ++actionIndex; } // The list has been built. Now call our action code... switch (action) { case "save": messenger.saveAllAttachments(attachmentContentTypeArray.length, attachmentContentTypeArray, attachmentUrlArray, attachmentDisplayNameArray, attachmentMessageUriArray); return; case "detach": // "detach" on a multiple selection of attachments is so far not really // supported. As a workaround, resort to normal detach-"all". See also // the comment on 'detaching a multiple selection of attachments' below. if (attachments.length == 1) attachments[0].detach(true); else messenger.detachAllAttachments(attachmentContentTypeArray.length, attachmentContentTypeArray, attachmentUrlArray, attachmentDisplayNameArray, attachmentMessageUriArray, true); // save return; case "delete": messenger.detachAllAttachments(attachmentContentTypeArray.length, attachmentContentTypeArray, attachmentUrlArray, attachmentDisplayNameArray, attachmentMessageUriArray, false); // don't save return; case "open": case "saveAs": // XXX hack alert. If we sit in tight loop and open/save multiple // attachments, we get chrome errors in layout as we start loading the // first helper app dialog then before it loads, we kick off the next // one and the next one. Subsequent helper app dialogs were failing // because we were still loading the chrome files for the first attempt // (error about the xul cache being empty). For now, work around this by // doing the first helper app dialog right away, then waiting a bit // before we launch the rest. var actionFunction = null; if (action == "open") actionFunction = function(aAttachment) { aAttachment.open(); }; else actionFunction = function(aAttachment) { aAttachment.save(); }; for (let i = 0; i < attachments.length; i++) { if (i == 0) actionFunction(attachments[i]); else setTimeout(actionFunction, 100, attachments[i]); } return; case "copyUrl": // Copy external http url(s) to clipboard. The menuitem is hidden unless // all selected attachment urls are http. let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"] .getService(Components.interfaces.nsIClipboardHelper); clipboard.copyString(attachmentUrlArray.join("\n")); return; default: throw new Error("unknown HandleMultipleAttachments action: " + action); } } function ClearAttachmentList() { // We also have to disable the Message/Attachments menuitem. var node = document.getElementById("msgAttachmentMenu"); if (node) node.setAttribute("disabled", "true"); // Do the same on appmenu. let appmenunode = document.getElementById("appmenu_msgAttachmentMenu"); if (appmenunode) appmenunode.setAttribute("disabled", "true"); // clear selection var list = document.getElementById("attachmentList"); list.clearSelection(); while (list.hasChildNodes()) list.lastChild.remove(); } var attachmentListDNDObserver = { onDragStart: function (aEvent, aAttachmentData, aDragAction) { let target = aEvent.target; if (target.localName == "attachmentitem") { let selection = target.parentNode.selectedItems; aAttachmentData.data = new TransferDataSet(); for (let item of selection) { let transferData = CreateAttachmentTransferData(item.attachment); if (transferData) aAttachmentData.data.push(transferData); } } } }; var attachmentNameDNDObserver = { onDragStart: function (aEvent, aAttachmentData, aDragAction) { var attachmentList = document.getElementById("attachmentList"); aAttachmentData.data = CreateAttachmentTransferData( attachmentList.getItemAtIndex(0).attachment); } }; /** * CopyWebsiteAddress takes the website address title button, extracts * the website address we stored in there and copies it to the clipboard */ function CopyWebsiteAddress(websiteAddressNode) { if (websiteAddressNode) { var websiteAddress = websiteAddressNode.textContent; var contractid = "@mozilla.org/widget/clipboardhelper;1"; var iid = Components.interfaces.nsIClipboardHelper; var clipboard = Components.classes[contractid].getService(iid); clipboard.copyString(websiteAddress); } } function nsDummyMsgHeader() { } nsDummyMsgHeader.prototype = { mProperties : new Array, getStringProperty : function(aProperty) { if (aProperty in this.mProperties) return this.mProperties[aProperty]; return ""; }, setStringProperty : function(aProperty, aVal) { this.mProperties[aProperty] = aVal; }, getUint32Property : function(aProperty) { if (aProperty in this.mProperties) return parseInt(this.mProperties[aProperty]); return 0; }, setUint32Property: function(aProperty, aVal) { this.mProperties[aProperty] = aVal.toString(); }, markHasAttachments : function(hasAttachments) {}, messageSize : 0, recipients : null, author: null, subject : "", get mime2DecodedSubject() { return this.subject; }, ccList : null, listPost : null, messageId : null, date : 0, accountKey : "", flags : 0, // If you change us to return a fake folder, please update // folderDisplay.js's FolderDisplayWidget's selectedMessageIsExternal getter. folder : null }; function onShowOtherActionsPopup() { // Enable/disable the Open Conversation button. let glodaEnabled = Services.prefs.getBoolPref("mailnews.database.global.indexer.enabled"); let openConversation = document.getElementById("otherActionsOpenConversation"); openConversation.disabled = !glodaEnabled; if (glodaEnabled && gFolderDisplay.selectedCount > 0) { let message = gFolderDisplay.selectedMessage; let isMessageIndexed = Gloda.isMessageIndexed(message); openConversation.disabled = !isMessageIndexed; } if (SelectedMessagesAreRead()) { document.getElementById("markAsReadMenuItem").setAttribute("hidden", true); document.getElementById("markAsUnreadMenuItem").removeAttribute("hidden"); } else { document.getElementById("markAsReadMenuItem").removeAttribute("hidden"); document.getElementById("markAsUnreadMenuItem").setAttribute("hidden", true); } } function ConversationOpener() { } ConversationOpener.prototype = { openConversationForMessages: function(messages) { if (messages.length < 1) return; try { this._items = []; this._msgHdr = messages[0]; this._queries = [Gloda.getMessageCollectionForHeaders(messages, this)]; } catch (e) { logException(e); } }, isSelectedMessageIndexed: function() { let glodaEnabled = Services.prefs .getBoolPref("mailnews.database.global.indexer.enabled"); if (glodaEnabled && gFolderDisplay.selectedCount > 0) { let message = gFolderDisplay.selectedMessage; return Gloda.isMessageIndexed(message); } return false; }, onItemsAdded: function(aItems) { }, onItemsModified: function(aItems) { }, onItemsRemoved: function(aItems) { }, onQueryCompleted: function(aCollection) { try { if (!aCollection.items.length) { Components.utils.reportError("Couldn't find a collection for msg: " + this._msgHdr); } else { let aMessage = aCollection.items[0]; let tabmail = document.getElementById("tabmail"); tabmail.openTab("glodaList", { conversation: aMessage.conversation, message: aMessage, title: aMessage.conversation.subject, background: false }); } } catch (e) { logException(e); } } } var gConversationOpener = new ConversationOpener();