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/mailnews/extensions/newsblog/FeedItem.jsm | |
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/mailnews/extensions/newsblog/FeedItem.jsm')
-rw-r--r-- | comm/mailnews/extensions/newsblog/FeedItem.jsm | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/comm/mailnews/extensions/newsblog/FeedItem.jsm b/comm/mailnews/extensions/newsblog/FeedItem.jsm new file mode 100644 index 0000000000..40a0424a0d --- /dev/null +++ b/comm/mailnews/extensions/newsblog/FeedItem.jsm @@ -0,0 +1,490 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 EXPORTED_SYMBOLS = ["FeedItem", "FeedEnclosure"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "FeedUtils", + "resource:///modules/FeedUtils.jsm" +); + +function FeedItem() { + this.mDate = lazy.FeedUtils.getValidRFC5322Date(); + this.mParserUtils = Cc["@mozilla.org/parserutils;1"].getService( + Ci.nsIParserUtils + ); +} + +FeedItem.prototype = { + // Only for IETF Atom. + xmlContentBase: null, + id: null, + feed: null, + description: null, + content: null, + enclosures: [], + title: null, + // Author must be angle bracket enclosed to function as an addr-spec, in the + // absence of an addr-spec portion of an RFC5322 email address, as other + // functionality (gloda search) depends on this. + author: "<anonymous>", + inReplyTo: "", + keywords: [], + mURL: null, + characterSet: "UTF-8", + + ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes + ENCLOSURE_HEADER_BOUNDARY_PREFIX: "------------", // 12 dashes + MESSAGE_TEMPLATE: + "\n" + + "<!DOCTYPE html>\n" + + "<html>\n" + + " <head>\n" + + " <title>%TITLE%</title>\n" + + ' <base href="%BASE%">\n' + + " </head>\n" + + ' <body id="msgFeedSummaryBody" selected="false">\n' + + " %CONTENT%\n" + + " </body>\n" + + "</html>\n", + + get url() { + return this.mURL; + }, + + set url(aVal) { + try { + this.mURL = Services.io.newURI(aVal).spec; + } catch (ex) { + // The url as published or constructed can be a non url. It's used as a + // feeditem identifier in feeditems.rdf, as a messageId, and as an href + // and for the content-base header. Save as is; ensure not null. + this.mURL = aVal ? aVal : ""; + } + }, + + get date() { + return this.mDate; + }, + + set date(aVal) { + this.mDate = aVal; + }, + + get identity() { + return this.feed.name + ": " + this.title + " (" + this.id + ")"; + }, + + normalizeMessageID(messageID) { + // Escape occurrences of message ID meta characters <, >, and @. + messageID.replace(/</g, "%3C"); + messageID.replace(/>/g, "%3E"); + messageID.replace(/@/g, "%40"); + messageID = "<" + messageID.trim() + "@localhost.localdomain>"; + + lazy.FeedUtils.log.trace( + "FeedItem.normalizeMessageID: messageID - " + messageID + ); + return messageID; + }, + + get contentBase() { + if (this.xmlContentBase) { + return this.xmlContentBase; + } + + return this.mURL; + }, + + /** + * Writes the item to the folder as a message and updates the feeditems db. + * + * @returns {void} + */ + store() { + // this.title and this.content contain HTML. + // this.mUrl and this.contentBase contain plain text. + + let stored = false; + let ds = lazy.FeedUtils.getItemsDS(this.feed.server); + let resource = this.findStoredResource(); + if (!this.feed.folder) { + return stored; + } + + if (resource == null) { + resource = { + feedURLs: [this.feed.url], + lastSeenTime: 0, + valid: false, + stored: false, + }; + ds.data[this.id] = resource; + if (!this.content) { + lazy.FeedUtils.log.trace( + "FeedItem.store: " + + this.identity + + " no content; storing description or title" + ); + this.content = this.description || this.title; + } + + let content = this.MESSAGE_TEMPLATE; + content = content.replace(/%TITLE%/, this.title); + content = content.replace(/%BASE%/, this.htmlEscape(this.contentBase)); + content = content.replace(/%CONTENT%/, this.content); + this.content = content; + this.writeToFolder(); + this.markStored(resource); + stored = true; + } + + this.markValid(resource); + ds.saveSoon(); + return stored; + }, + + findStoredResource() { + // Checks to see if the item has already been stored in its feed's + // message folder. + lazy.FeedUtils.log.trace( + "FeedItem.findStoredResource: checking if stored - " + this.identity + ); + + let server = this.feed.server; + let folder = this.feed.folder; + + if (!folder) { + lazy.FeedUtils.log.debug( + "FeedItem.findStoredResource: folder '" + + this.feed.folderName + + "' doesn't exist; creating as child of " + + server.rootMsgFolder.prettyName + + "\n" + ); + this.feed.createFolder(); + return null; + } + + let ds = lazy.FeedUtils.getItemsDS(server); + let item = ds.data[this.id]; + if (!item || !item.stored) { + lazy.FeedUtils.log.trace("FeedItem.findStoredResource: not stored"); + return null; + } + + lazy.FeedUtils.log.trace("FeedItem.findStoredResource: already stored"); + return item; + }, + + markValid(resource) { + resource.lastSeenTime = new Date().getTime(); + // Items can be in multiple feeds. + if (!resource.feedURLs.includes(this.feed.url)) { + resource.feedURLs.push(this.feed.url); + } + resource.valid = true; + }, + + markStored(resource) { + // Items can be in multiple feeds. + if (!resource.feedURLs.includes(this.feed.url)) { + resource.feedURLs.push(this.feed.url); + } + resource.stored = true; + }, + + writeToFolder() { + lazy.FeedUtils.log.trace( + "FeedItem.writeToFolder: " + + this.identity + + " writing to message folder " + + this.feed.name + ); + // The subject may contain HTML entities. Convert these to their unencoded + // state. i.e. & becomes '&'. + let title = this.title; + title = this.mParserUtils.convertToPlainText( + title, + Ci.nsIDocumentEncoder.OutputSelectionOnly | + Ci.nsIDocumentEncoder.OutputAbsoluteLinks, + 0 + ); + + // Compress white space in the subject to make it look better. Trim + // leading/trailing spaces to prevent mbox header folding issue at just + // the right subject length. + this.title = title.replace(/[\t\r\n]+/g, " ").trim(); + + // If the date looks like it's in W3C-DTF format, convert it into + // an IETF standard date. Otherwise assume it's in IETF format. + if (this.mDate.search(/^\d\d\d\d/) != -1) { + this.mDate = new Date(this.mDate).toUTCString(); + } + + // If there is an inreplyto value, create the headers. + let inreplytoHdrsStr = this.inReplyTo + ? "References: " + + this.inReplyTo + + "\n" + + "In-Reply-To: " + + this.inReplyTo + + "\n" + : ""; + + // Support multiple authors in From. + let fromStr = this.createHeaderStrFromArray("From: ", this.author); + + // If there are keywords (categories), create the headers. + let keywordsStr = this.createHeaderStrFromArray( + "Keywords: ", + this.keywords + ); + + // Escape occurrences of "From " at the beginning of lines of + // content per the mbox standard, since "From " denotes a new + // message, and add a line break so we know the last line has one. + this.content = this.content.replace(/([\r\n]+)(>*From )/g, "$1>$2"); + this.content += "\n"; + + let source = + "From - " + + this.mDate + + "\n" + + "X-Mozilla-Status: 0000\n" + + "X-Mozilla-Status2: 00000000\n" + + "X-Mozilla-Keys: " + + " ".repeat(80) + + "\n" + + "Received: by localhost; " + + lazy.FeedUtils.getValidRFC5322Date() + + "\n" + + "Date: " + + this.mDate + + "\n" + + "Message-Id: " + + this.normalizeMessageID(this.id) + + "\n" + + fromStr + + "MIME-Version: 1.0\n" + + "Subject: " + + this.title + + "\n" + + inreplytoHdrsStr + + keywordsStr + + "Content-Transfer-Encoding: 8bit\n" + + "Content-Base: " + + this.mURL + + "\n"; + + if (this.enclosures.length) { + let boundaryID = source.length; + source += + 'Content-Type: multipart/mixed; boundary="' + + this.ENCLOSURE_HEADER_BOUNDARY_PREFIX + + boundaryID + + '"\n\n' + + "This is a multi-part message in MIME format.\n" + + this.ENCLOSURE_BOUNDARY_PREFIX + + boundaryID + + "\n" + + "Content-Type: text/html; charset=" + + this.characterSet + + "\n" + + "Content-Transfer-Encoding: 8bit\n" + + this.content; + + this.enclosures.forEach(function (enclosure) { + source += enclosure.convertToAttachment(boundaryID); + }); + + source += this.ENCLOSURE_BOUNDARY_PREFIX + boundaryID + "--\n\n\n"; + } else { + source += + "Content-Type: text/html; charset=" + + this.characterSet + + "\n" + + this.content; + } + + lazy.FeedUtils.log.trace( + "FeedItem.writeToFolder: " + + this.identity + + " is " + + source.length + + " characters long" + ); + + // Get the folder and database storing the feed's messages and headers. + let folder = this.feed.folder.QueryInterface(Ci.nsIMsgLocalMailFolder); + let msgFolder = folder.QueryInterface(Ci.nsIMsgFolder); + msgFolder.gettingNewMessages = true; + // Source is a unicode js string, as UTF-16, and we want to save a + // char * cpp |string| as UTF-8 bytes. The source xml doc encoding is utf8. + source = unescape(encodeURIComponent(source)); + let msgDBHdr = folder.addMessage(source); + msgDBHdr.orFlags(Ci.nsMsgMessageFlags.FeedMsg); + msgFolder.gettingNewMessages = false; + this.tagItem(msgDBHdr, this.keywords); + }, + + /** + * Create a header string from an array. Intended for comma separated headers + * like From or Keywords. In the case of a longer than RFC5322 recommended + * line length, create multiple folded lines (easier to parse than multiple + * headers). + * + * @param {string} headerName - Name of the header. + * @param {string[]} headerItemsArray - An Array of strings to concatenate. + * + * @returns {String} - The header string. + */ + createHeaderStrFromArray(headerName, headerItemsArray) { + let headerStr = ""; + if (!headerItemsArray || headerItemsArray.length == 0) { + return headerStr; + } + + const HEADER = headerName; + const LINELENGTH = 78; + const MAXLINELENGTH = 990; + let items = [].concat(headerItemsArray); + let lines = []; + headerStr = HEADER; + while (items.length) { + let item = items.shift(); + if ( + headerStr.length + item.length > LINELENGTH && + headerStr.length > HEADER.length + ) { + lines.push(headerStr); + headerStr = " ".repeat(HEADER.length); + } + + headerStr += + headerStr.length + item.length > MAXLINELENGTH + ? item.substr(0, MAXLINELENGTH - headerStr.length) + "…, " + : item + ", "; + } + + headerStr = headerStr.replace(/,\s$/, "\n"); + lines.push(headerStr); + headerStr = lines.join("\n"); + + return headerStr; + }, + + /** + * Autotag messages. + * + * @param {nsIMsgDBHdr} aMsgDBHdr - message to tag + * @param {Array} aKeywords - keywords (tags) + * @returns {void} + */ + tagItem(aMsgDBHdr, aKeywords) { + let category = this.feed.options.category; + if (!aKeywords.length || !category.enabled) { + return; + } + + let prefix = category.prefixEnabled ? category.prefix : ""; + let rtl = Services.prefs.getIntPref("bidi.direction") == 2; + + let keys = []; + for (let keyword of aKeywords) { + keyword = rtl ? keyword + prefix : prefix + keyword; + let keyForTag = MailServices.tags.getKeyForTag(keyword); + if (!keyForTag) { + // Add the tag if it doesn't exist. + MailServices.tags.addTag(keyword, "", lazy.FeedUtils.AUTOTAG); + keyForTag = MailServices.tags.getKeyForTag(keyword); + } + + // Add the tag key to the keys array. + keys.push(keyForTag); + } + + if (keys.length) { + // Add the keys to the message. + aMsgDBHdr.folder.addKeywordsToMessages([aMsgDBHdr], keys.join(" ")); + } + }, + + htmlEscape(s) { + s = s.replace(/&/g, "&"); + s = s.replace(/>/g, ">"); + s = s.replace(/</g, "<"); + s = s.replace(/'/g, "'"); + s = s.replace(/"/g, """); + return s; + }, +}; + +// A feed enclosure is to RSS what an attachment is for e-mail. We make +// enclosures look like attachments in the UI. +function FeedEnclosure(aURL, aContentType, aLength, aTitle) { + this.mURL = aURL; + // Store a reasonable mimetype if content-type is not present. + this.mContentType = aContentType || "application/unknown"; + this.mLength = aLength; + this.mTitle = aTitle; + + // Generate a fileName from the URL. + if (this.mURL) { + try { + let uri = Services.io.newURI(this.mURL).QueryInterface(Ci.nsIURL); + this.mFileName = uri.fileName; + // Determine mimetype from extension if content-type is not present. + if (!aContentType) { + let contentType = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromExtension(uri.fileExtension); + this.mContentType = contentType; + } + } catch (ex) { + this.mFileName = this.mURL; + } + } +} + +FeedEnclosure.prototype = { + mURL: "", + mContentType: "", + mLength: 0, + mFileName: "", + mTitle: "", + ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes + + // Returns a string that looks like an e-mail attachment which represents + // the enclosure. + convertToAttachment(aBoundaryID) { + return ( + "\n" + + this.ENCLOSURE_BOUNDARY_PREFIX + + aBoundaryID + + "\n" + + "Content-Type: " + + this.mContentType + + '; name="' + + (this.mTitle || this.mFileName) + + (this.mLength ? '"; size=' + this.mLength : '"') + + "\n" + + "X-Mozilla-External-Attachment-URL: " + + this.mURL + + "\n" + + 'Content-Disposition: attachment; filename="' + + this.mFileName + + '"\n\n' + + lazy.FeedUtils.strings.GetStringFromName("externalAttachmentMsg") + + "\n" + ); + }, +}; |