/* -*- 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/. */ function FeedItem() { this.mDate = FeedUtils.getValidRFC5322Date(); this.mUnicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. createInstance(Ci.nsIScriptableUnicodeConverter); 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: "anonymous", inReplyTo: "", keywords: [], mURL: null, characterSet: "UTF-8", ENCLOSURE_BOUNDARY_PREFIX: "--------------", // 14 dashes ENCLOSURE_HEADER_BOUNDARY_PREFIX: "------------", // 12 dashes MESSAGE_TEMPLATE: '\n' + '\n' + ' \n' + ' %TITLE%\n' + ' \n' + ' \n' + ' \n' + ' %CONTENT%\n' + ' \n' + '\n', get url() { return this.mURL; }, set url(aVal) { try { this.mURL = Services.io.newURI(aVal, null, null).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: function(messageID) { // Escape occurrences of message ID meta characters <, >, and @. messageID.replace(//g, "%3E"); messageID.replace(/@/g, "%40"); messageID = "<" + messageID.trim() + "@" + "localhost.localdomain" + ">"; FeedUtils.log.trace("FeedItem.normalizeMessageID: messageID - " + messageID); return messageID; }, get itemUniqueURI() { return this.createURN(this.id); }, get contentBase() { if(this.xmlContentBase) return this.xmlContentBase else return this.mURL; }, store: function() { // this.title and this.content contain HTML. // this.mUrl and this.contentBase contain plain text. let stored = false; let resource = this.findStoredResource(); if (!this.feed.folder) return stored; if (resource == null) { resource = FeedUtils.rdf.GetResource(this.itemUniqueURI); if (!this.content) { 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); return stored; }, findStoredResource: function() { // Checks to see if the item has already been stored in its feed's // message folder. FeedUtils.log.trace("FeedItem.findStoredResource: checking if stored - " + this.identity); let server = this.feed.server; let folder = this.feed.folder; if (!folder) { 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 = FeedUtils.getItemsDS(server); let itemURI = this.itemUniqueURI; let itemResource = FeedUtils.rdf.GetResource(itemURI); let downloaded = ds.GetTarget(itemResource, FeedUtils.FZ_STORED, true); if (!downloaded || downloaded.QueryInterface(Ci.nsIRDFLiteral).Value == "false") { FeedUtils.log.trace("FeedItem.findStoredResource: not stored"); return null; } FeedUtils.log.trace("FeedItem.findStoredResource: already stored"); return itemResource; }, markValid: function(resource) { let ds = FeedUtils.getItemsDS(this.feed.server); let newTimeStamp = FeedUtils.rdf.GetLiteral(new Date().getTime()); let currentTimeStamp = ds.GetTarget(resource, FeedUtils.FZ_LAST_SEEN_TIMESTAMP, true); if (currentTimeStamp) ds.Change(resource, FeedUtils.FZ_LAST_SEEN_TIMESTAMP, currentTimeStamp, newTimeStamp); else ds.Assert(resource, FeedUtils.FZ_LAST_SEEN_TIMESTAMP, newTimeStamp, true); if (!ds.HasAssertion(resource, FeedUtils.FZ_FEED, FeedUtils.rdf.GetResource(this.feed.url), true)) ds.Assert(resource, FeedUtils.FZ_FEED, FeedUtils.rdf.GetResource(this.feed.url), true); if (ds.hasArcOut(resource, FeedUtils.FZ_VALID)) { let currentValue = ds.GetTarget(resource, FeedUtils.FZ_VALID, true); ds.Change(resource, FeedUtils.FZ_VALID, currentValue, FeedUtils.RDF_LITERAL_TRUE); } else ds.Assert(resource, FeedUtils.FZ_VALID, FeedUtils.RDF_LITERAL_TRUE, true); }, markStored: function(resource) { let ds = FeedUtils.getItemsDS(this.feed.server); if (!ds.HasAssertion(resource, FeedUtils.FZ_FEED, FeedUtils.rdf.GetResource(this.feed.url), true)) ds.Assert(resource, FeedUtils.FZ_FEED, FeedUtils.rdf.GetResource(this.feed.url), true); let currentValue; if (ds.hasArcOut(resource, FeedUtils.FZ_STORED)) { currentValue = ds.GetTarget(resource, FeedUtils.FZ_STORED, true); ds.Change(resource, FeedUtils.FZ_STORED, currentValue, FeedUtils.RDF_LITERAL_TRUE); } else ds.Assert(resource, FeedUtils.FZ_STORED, FeedUtils.RDF_LITERAL_TRUE, true); }, mimeEncodeSubject: function(aSubject, aCharset) { // This routine sometimes throws exceptions for mis-encoded data so // wrap it with a try catch for now. let newSubject; try { newSubject = mailServices.mimeConverter.encodeMimePartIIStr_UTF8(aSubject, false, aCharset, 9, 72); } catch (ex) { newSubject = aSubject; } return newSubject; }, writeToFolder: function() { FeedUtils.log.trace("FeedItem.writeToFolder: " + this.identity + " writing to message folder " + this.feed.name); // Convert the title to UTF-16 before performing our HTML entity // replacement reg expressions. let title = this.title; // The subject may contain HTML entities. Convert these to their unencoded // state. i.e. & becomes '&'. 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. title = title.replace(/[\t\r\n]+/g, " ").trim(); this.title = this.mimeEncodeSubject(title, this.characterSet); // 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") : ""; // If there are keywords (categories), create the headers. In the case of // a longer than RFC5322 recommended line length, create multiple folded // lines (easier to parse than multiple Keywords headers). let keywordsStr = ""; if (this.keywords.length) { let HEADER = "Keywords: "; let MAXLEN = 78; keywordsStr = HEADER; let keyword; let keywords = [].concat(this.keywords); let lines = []; while (keywords.length) { keyword = keywords.shift(); if (keywordsStr.length + keyword.length > MAXLEN) { lines.push(keywordsStr) keywordsStr = " ".repeat(HEADER.length); } keywordsStr += keyword + ","; } keywordsStr = keywordsStr.replace(/,$/,"\n"); lines.push(keywordsStr) keywordsStr = lines.join("\n"); } // 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"; // The opening line of the message, mandated by standards to start // with "From ". It's useful to construct this separately because // we not only need to write it into the message, we also need to // use it to calculate the offset of the X-Mozilla-Status lines from // the front of the message for the statusOffset property of the // DB header object. let openingLine = 'From - ' + this.mDate + '\n'; let source = openingLine + 'X-Mozilla-Status: 0000\n' + 'X-Mozilla-Status2: 00000000\n' + 'X-Mozilla-Keys: ' + " ".repeat(80) + '\n' + 'Received: by localhost; ' + FeedUtils.getValidRFC5322Date() + '\n' + 'Date: ' + this.mDate + '\n' + 'Message-Id: ' + this.normalizeMessageID(this.id) + '\n' + 'From: ' + this.author + '\n' + '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; 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 string, we want to save a char * string in // the original charset. So convert back. this.mUnicodeConverter.charset = this.characterSet; let msgDBHdr = folder.addMessage(this.mUnicodeConverter.ConvertFromUnicode(source)); msgDBHdr.OrFlags(Ci.nsMsgMessageFlags.FeedMsg); msgFolder.gettingNewMessages = false; this.tagItem(msgDBHdr, this.keywords); }, /** * Autotag messages. * * @param nsIMsgDBHdr aMsgDBHdr - message to tag * @param array aKeywords - keywords (tags) */ tagItem: function(aMsgDBHdr, aKeywords) { let categoryPrefs = this.feed.categoryPrefs(); if (!aKeywords.length || !categoryPrefs.enabled) return; let msgArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); msgArray.appendElement(aMsgDBHdr, false); let prefix = categoryPrefs.prefixEnabled ? categoryPrefs.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, "", 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(msgArray, keys.join(" ")); }, htmlEscape: function(s) { s = s.replace(/&/g, "&"); s = s.replace(/>/g, ">"); s = s.replace(/