!import
1 //@line 37 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/mail/extensions/newsblog/content/FeedItem.js"
2
3 // Handy conversion values.
4 const HOURS_TO_MINUTES = 60;
5 const MINUTES_TO_SECONDS = 60;
6 const SECONDS_TO_MILLISECONDS = 1000;
7 const MINUTES_TO_MILLISECONDS = MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS;
8 const HOURS_TO_MILLISECONDS = HOURS_TO_MINUTES * MINUTES_TO_MILLISECONDS;
9 const MSG_FLAG_NEW = 0x10000;
10 const ENCLOSURE_BOUNDARY_PREFIX = "--------------"; // 14 dashes
11 const ENCLOSURE_HEADER_BOUNDARY_PREFIX = "------------"; // 12 dashes
12
13 const MESSAGE_TEMPLATE = "\n\
14 <html>\n\
15 <head>\n\
16 <title>%TITLE%</title>\n\
17 <base href=\"%BASE%\">\n\
18 <style type=\"text/css\">\n\
19 %STYLE%\n\
20 </style>\n\
21 </head>\n\
22 <body>\n\
23 %CONTENT_TEMPLATE%\n\
24 </body>\n\
25 </html>\n\
26 ";
27
28 const REMOTE_CONTENT_TEMPLATE = "\n\
29 <iframe id =\"_mailrssiframe\" src=\"%URL%\">\n\
30 %DESCRIPTION%\n\
31 </iframe>\n\
32 ";
33
34 const REMOTE_STYLE = "\n\
35 body {\n\
36 margin: 0;\n\
37 border: none;\n\
38 padding: 0;\n\
39 }\n\
40 iframe {\n\
41 position: fixed;\n\
42 top: 0;\n\
43 left: 0;\n\
44 width: 100%;\n\
45 height: 100%;\n\
46 border: none;\n\
47 }\n\
48 ";
49
50 // Unlike remote content, which is locked within a fixed position iframe,
51 // local content goes is positioned according to the normal rules of flow.
52 // The problem with this is that the message pane itself provides a scrollbar
53 // if necessary, and that scrollbar appears next to the toolbar as well as
54 // the content being scrolled. The solution is to lock local content within
55 // a fixed position div and set its overflow property to auto so that the div
56 // itself provides the scrollbar. Unfortunately we can't do that because of
57 // Mozilla bug 97283, which makes it hard to scroll an auto overflow div.
58
59 const LOCAL_CONTENT_TEMPLATE = "\n\
60 %CONTENT%\n\
61 ";
62
63 // no local style overrides at this time
64 const LOCAL_STYLE = "\n";
65
FeedItem
66 function FeedItem()
67 {
68 this.mDate = new Date().toString();
69 this.mUnicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
70 .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
71 }
72
73 FeedItem.prototype =
74 {
75 isStoredWithId: false, // we currently only do this for IETF Atom. RSS2 with GUIDs should do this as well.
76 xmlContentBase: null, // only for IETF Atom
77 id: null,
78 feed: null,
79 description: null,
80 content: null,
81 enclosure: null, // we currently only support one enclosure per feed item...
82 title: "(no subject)", // TO DO: this needs to be localized
83 author: "anonymous",
84 mURL: null,
85 characterSet: "",
86
get_url
87 get url()
88 {
89 return this.mURL;
90 },
91
set_url
92 set url(aVal)
93 {
94 var uri = Components.classes["@mozilla.org/network/standard-url;1"].getService(Components.interfaces["nsIStandardURL"]);
95 uri.init(1, 80, aVal, null, null);
96 var uri = uri.QueryInterface(Components.interfaces.nsIURI);
97 this.mURL = uri.spec;
98 },
99
get_date
100 get date()
101 {
102 return this.mDate;
103 },
104
set_date
105 set date (aVal)
106 {
107 this.mDate = aVal;
108 },
109
get_identity
110 get identity ()
111 {
112 return this.feed.name + ": " + this.title + " (" + this.id + ")"
113 },
114
get_messageID
115 get messageID()
116 {
117 var messageID = this.id || this.mURL || this.title;
118
119 // Escape occurrences of message ID meta characters <, >, and @.
120 messageID.replace(/</g, "%3C");
121 messageID.replace(/>/g, "%3E");
122 messageID.replace(/@/g, "%40");
123 messageID = messageID + "@" + "localhost.localdomain";
124 return messageID;
125 },
126
get_itemUniqueURI
127 get itemUniqueURI()
128 {
129 var theURI;
130 if(this.isStoredWithId && this.id)
131 theURI = "urn:" + this.id;
132 else
133 theURI = this.mURL || ("urn:" + this.id);
134 return theURI;
135 },
136
get_contentBase
137 get contentBase()
138 {
139 if(this.xmlContentBase)
140 return this.xmlContentBase
141 else
142 return this.mURL;
143 },
144
store
145 store: function()
146 {
147 this.mUnicodeConverter.charset = this.characterSet;
148
149 if (this.isStored())
150 debug(this.identity + " already stored; ignoring");
151 else if (this.content)
152 {
153 debug(this.identity + " has content; storing");
154 var content = MESSAGE_TEMPLATE;
155 content = content.replace(/%CONTENT_TEMPLATE%/, LOCAL_CONTENT_TEMPLATE);
156 content = content.replace(/%STYLE%/, LOCAL_STYLE);
157 content = content.replace(/%TITLE%/, this.title);
158 content = content.replace(/%BASE%/, this.contentBase);
159 content = content.replace(/%URL%/g, this.mURL);
160 content = content.replace(/%CONTENT%/, this.content);
161 this.content = content; // XXX store it elsewhere, f.e. this.page
162 this.writeToFolder();
163 }
164 else if (this.feed.quickMode || !this.mURL)
165 {
166 debug(this.identity + " in quick mode; storing");
167
168 this.content = this.description || this.title;
169
170 var content = MESSAGE_TEMPLATE;
171 content = content.replace(/%CONTENT_TEMPLATE%/, LOCAL_CONTENT_TEMPLATE);
172 content = content.replace(/%STYLE%/, LOCAL_STYLE);
173 content = content.replace(/%BASE%/, this.contentBase);
174 content = content.replace(/%TITLE%/, this.title);
175 content = content.replace(/%URL%/g, this.mURL);
176 content = content.replace(/%CONTENT%/, this.content);
177 this.content = content; // XXX store it elsewhere, f.e. this.page
178 this.writeToFolder();
179 }
180 else
181 {
182 //debug(this.identity + " needs content; downloading");
183 debug(this.identity + " needs content; creating and storing");
184 var content = MESSAGE_TEMPLATE;
185 content = content.replace(/%CONTENT_TEMPLATE%/, REMOTE_CONTENT_TEMPLATE);
186 content = content.replace(/%STYLE%/, REMOTE_STYLE);
187 content = content.replace(/%TITLE%/, this.title);
188 content = content.replace(/%BASE%/, this.contentBase);
189 content = content.replace(/%URL%/g, this.mURL);
190 content = content.replace(/%DESCRIPTION%/, this.description || this.title);
191 this.content = content; // XXX store it elsewhere, f.e. this.page
192 this.writeToFolder();
193 }
194 },
195
isStored
196 isStored: function()
197 {
198 // Checks to see if the item has already been stored in its feed's message folder.
199
200 debug(this.identity + " checking to see if stored");
201
202 var server = this.feed.server;
203 var folder = this.feed.folder;
204
205 if (!folder)
206 {
207 debug(this.feed.name + " folder doesn't exist; creating");
208 debug("creating " + this.feed.name + "as child of " + server.rootMsgFolder + "\n");
209 server.rootMsgFolder.createSubfolder(this.feed.name, null /* supposed to be a msg window */);
210 folder = server.rootMsgFolder.FindSubFolder(this.feed.name);
211 debug(this.identity + " not stored (folder didn't exist)");
212 return false;
213 }
214
215 var ds = getItemsDS(server);
216 var itemURI = this.itemUniqueURI;
217 var itemResource = rdf.GetResource(itemURI);
218
219 var downloaded = ds.GetTarget(itemResource, FZ_STORED, true);
220
221 // Backward compatibility: we might have stored this item before isStoredWithId
222 // has been turned on for RSS 2.0 (bug 354345). Check whether this item has been
223 // stored with its URL.
224 if (!downloaded && this.mURL && itemURI != this.mURL)
225 {
226 itemResource = rdf.GetResource(this.mURL);
227 downloaded = ds.GetTarget(itemResource, FZ_STORED, true);
228 }
229
230 if (!downloaded || downloaded.QueryInterface(Components.interfaces.nsIRDFLiteral).Value == "false")
231 {
232 // HACK ALERT: before we give up, try to work around an entity escaping bug in RDF
233 // See Bug #258465 for more details
234 itemURI = itemURI.replace(/</g, '<');
235 itemURI = itemURI.replace(/>/g, '>');
236 itemURI = itemURI.replace(/"/g, '"');
237 itemURI = itemURI.replace(/&/g, '&');
238
239 debug('Failed to find item, trying entity replacement version: ' + itemURI);
240 itemResource = rdf.GetResource(itemURI);
241 downloaded = ds.GetTarget(itemResource, FZ_STORED, true);
242
243 if (downloaded)
244 {
245 debug(this.identity + " not stored");
246 return true;
247 }
248
249 debug(this.identity + " not stored");
250 return false;
251 }
252 else
253 {
254 debug(this.identity + " stored");
255 return true;
256 }
257 },
258
markValid
259 markValid: function()
260 {
261 debug("validating " + this.mURL);
262 var ds = getItemsDS(this.feed.server);
263
264 var itemURI = this.itemUniqueURI;
265 var resource = rdf.GetResource(itemURI);
266
267 // Backward compatibility: we might have stored this item before isStoredWithId
268 // has been turned on for RSS 2.0 (bug 354345). Check whether this item has been
269 // stored with its URL.
270 if (!ds.GetTarget(resource, FZ_STORED, true) && this.mURL && itemURI != this.mURL)
271 resource = rdf.GetResource(this.mURL);
272
273 if (!ds.HasAssertion(resource, FZ_FEED, rdf.GetResource(this.feed.url), true))
274 ds.Assert(resource, FZ_FEED, rdf.GetResource(this.feed.url), true);
275
276 if (ds.hasArcOut(resource, FZ_VALID))
277 {
278 var currentValue = ds.GetTarget(resource, FZ_VALID, true);
279 ds.Change(resource, FZ_VALID, currentValue, RDF_LITERAL_TRUE);
280 }
281 else
282 ds.Assert(resource, FZ_VALID, RDF_LITERAL_TRUE, true);
283 },
284
markStored
285 markStored: function()
286 {
287 var ds = getItemsDS(this.feed.server);
288 var itemURI = this.itemUniqueURI;
289 var resource = rdf.GetResource(itemURI);
290
291 if (!ds.HasAssertion(resource, FZ_FEED, rdf.GetResource(this.feed.url), true))
292 ds.Assert(resource, FZ_FEED, rdf.GetResource(this.feed.url), true);
293
294 var currentValue;
295 if (ds.hasArcOut(resource, FZ_STORED))
296 {
297 currentValue = ds.GetTarget(resource, FZ_STORED, true);
298 ds.Change(resource, FZ_STORED, currentValue, RDF_LITERAL_TRUE);
299 }
300 else
301 ds.Assert(resource, FZ_STORED, RDF_LITERAL_TRUE, true);
302 },
303
mimeEncodeSubject
304 mimeEncodeSubject: function(aSubject, aCharset)
305 {
306 // get the mime header encoder service
307 var mimeEncoder = Components.classes["@mozilla.org/messenger/mimeconverter;1"].getService(Components.interfaces.nsIMimeConverter);
308
309 // this routine sometimes throws exceptions for mis-encoded data so wrap it
310 // with a try catch for now..
311 var newSubject;
312 try
313 {
314 newSubject = mimeEncoder.encodeMimePartIIStr(this.mUnicodeConverter.ConvertFromUnicode(aSubject), false, aCharset, 9, 72);
315 }
316 catch (ex)
317 {
318 newSubject = aSubject;
319 }
320
321 return newSubject;
322 },
323
writeToFolder
324 writeToFolder: function()
325 {
326 debug(this.identity + " writing to message folder" + this.feed.name + "\n");
327
328 var server = this.feed.server;
329 this.mUnicodeConverter.charset = this.characterSet;
330
331 // If the sender isn't a valid email address, quote it so it looks nicer.
332 if (this.author && this.author.indexOf('@') == -1)
333 this.author = '<' + this.author + '>';
334
335 // Convert the title to UTF-16 before performing our HTML entity replacement
336 // reg expressions.
337 var title = this.title;
338
339 // the subject may contain HTML entities.
340 // Convert these to their unencoded state. i.e. & becomes '&'
341 title = title.replace(/</g, '<');
342 title = title.replace(/>/g, '>');
343 title = title.replace(/"/g, '"');
344 title = title.replace(/&/g, '&');
345
346 // Compress white space in the subject to make it look better.
347 title = title.replace(/[\t\r\n]+/g, " ");
348
349 this.title = this.mimeEncodeSubject(title, this.characterSet);
350
351 // If the date looks like it's in W3C-DTF format, convert it into
352 // an IETF standard date. Otherwise assume it's in IETF format.
353 if (this.mDate.search(/^\d\d\d\d/) != -1)
354 this.mDate = W3CToIETFDate(this.mDate);
355
356 // Escape occurrences of "From " at the beginning of lines of content
357 // per the mbox standard, since "From " denotes a new message, and add
358 // a line break so we know the last line has one.
359 this.content = this.content.replace(/([\r\n]+)(>*From )/g, "$1>$2");
360 this.content += "\n";
361
362 // The opening line of the message, mandated by standards to start with
363 // "From ". It's useful to construct this separately because we not only
364 // need to write it into the message, we also need to use it to calculate
365 // the offset of the X-Mozilla-Status lines from the front of the message
366 // for the statusOffset property of the DB header object.
367 var openingLine = 'From - ' + this.mDate + '\n';
368
369 var source =
370 openingLine +
371 'X-Mozilla-Status: 0000\n' +
372 'X-Mozilla-Status2: 00000000\n' +
373 'X-Mozilla-Keys: \n' +
374 'Date: ' + this.mDate + '\n' +
375 'Message-Id: <' + this.messageID + '>\n' +
376 'From: ' + this.author + '\n' +
377 'MIME-Version: 1.0\n' +
378 'Subject: ' + this.title + '\n' +
379 'Content-Transfer-Encoding: 8bit\n' +
380 'Content-Base: ' + this.mURL + '\n';
381
382 if (this.enclosure && this.enclosure.mFileName)
383 {
384 var boundaryID = source.length + this.enclosure.mLength;
385 source += 'Content-Type: multipart/mixed;\n boundary="' + ENCLOSURE_HEADER_BOUNDARY_PREFIX + boundaryID + '"' + '\n\n' +
386 'This is a multi-part message in MIME format.\n' + ENCLOSURE_BOUNDARY_PREFIX + boundaryID + '\n' +
387 'Content-Type: text/html; charset=' + this.characterSet + '\n' +
388 'Content-Transfer-Encoding: 8bit\n' +
389 this.content;
390 source += this.enclosure.convertToAttachment(boundaryID);
391 }
392 else
393 {
394 source += 'Content-Type: text/html; charset=' + this.characterSet + '\n' +
395 '\n' + this.content;
396
397 }
398
399 debug(this.identity + " is " + source.length + " characters long");
400
401 // Get the folder and database storing the feed's messages and headers.
402 folder = this.feed.folder.QueryInterface(Components.interfaces.nsIMsgLocalMailFolder);
403 var msgFolder = folder.QueryInterface(Components.interfaces.nsIMsgFolder);
404 msgFolder.gettingNewMessages = true;
405 // source is a unicode string, we want to save a char * string in the original charset. So convert back
406 folder.addMessage(this.mUnicodeConverter.ConvertFromUnicode(source));
407 msgFolder.gettingNewMessages = false;
408 this.markStored();
409 }
410 };
411
412
413 // A feed enclosure is to RSS what an attachment is for e-mail. We make enclosures look
414 // like attachments in the UI.
415
FeedEnclosure
416 function FeedEnclosure(aURL, aContentType, aLength)
417 {
418 this.mURL = aURL;
419 this.mContentType = aContentType;
420 this.mLength = aLength;
421
422 // generate a fileName from the URL
423 if (this.mURL)
424 {
425 var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
426 var enclosureURL = ioService.newURI(this.mURL, null, null).QueryInterface(Components.interfaces.nsIURL);
427 if (enclosureURL)
428 this.mFileName = enclosureURL.fileName;
429 }
430 }
431
432 FeedEnclosure.prototype =
433 {
434 mURL: "",
435 mContentType: "",
436 mLength: 0,
437 mFileName: "",
438
439 // returns a string that looks like an e-mail attachment
440 // which represents the enclosure.
convertToAttachment
441 convertToAttachment: function(aBoundaryID)
442 {
443 return '\n' +
444 ENCLOSURE_BOUNDARY_PREFIX + aBoundaryID + '\n' +
445 'Content-Type: ' + this.mContentType + '; name="' + this.mFileName + '"\n' +
446 'X-Mozilla-External-Attachment-URL: ' + this.mURL + '\n' +
447 'Content-Disposition: attachment; filename="' + this.mFileName + '"\n\n' +
448 'This MIME attachment is stored separately from the message.\n' +
449 ENCLOSURE_BOUNDARY_PREFIX + aBoundaryID + '--' + '\n';
450
451 }
452 };