/* -*- 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(/