1 /* ***** BEGIN LICENSE BLOCK *****
  2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3  *
  4  * The contents of this file are subject to the Mozilla Public License Version
  5  * 1.1 (the "License"); you may not use this file except in compliance with
  6  * the License. You may obtain a copy of the License at
  7  * http://www.mozilla.org/MPL/
  8  *
  9  * Software distributed under the License is distributed on an "AS IS" basis,
 10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 11  * for the specific language governing rights and limitations under the
 12  * License.
 13  *
 14  * The Original Code is Thunderbird Conversations
 15  *
 16  * The Initial Developer of the Original Code is
 17  * Jonathan Protzenko
 18  * Portions created by the Initial Developer are Copyright (C) 2010
 19  * the Initial Developer. All Rights Reserved.
 20  *
 21  * Contributor(s):
 22  *
 23  * Alternatively, the contents of this file may be used under the terms of
 24  * either the GNU General Public License Version 2 or later (the "GPL"), or
 25  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 26  * in which case the provisions of the GPL or the LGPL are applicable instead
 27  * of those above. If you wish to allow use of your version of this file only
 28  * under the terms of either the GPL or the LGPL, and not to allow others to
 29  * use your version of this file under the terms of the MPL, indicate your
 30  * decision by deleting the provisions above and replace them with the notice
 31  * and other provisions required by the GPL or the LGPL. If you do not delete
 32  * the provisions above, a recipient may use your version of this file under
 33  * the terms of any one of the MPL, the GPL or the LGPL.
 34  *
 35  * ***** END LICENSE BLOCK ***** */
 36 
 37 /**
 38  * @fileoverview Composition-related utils: quoting, wrapping text before
 39  *  sending a message, converting back and forth between HTML and plain text...
 40  * @author Jonathan Protzenko
 41  */
 42 
 43 var EXPORTED_SYMBOLS = [
 44   'quoteMsgHdr', 'citeString',
 45   'htmlToPlainText', 'simpleWrap',
 46   'plainTextToHtml',
 47 ]
 48 
 49 const Ci = Components.interfaces;
 50 const Cc = Components.classes;
 51 const Cu = Components.utils;
 52 const Cr = Components.results;
 53 
 54 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); // for generateQI
 55 Cu.import("resource://gre/modules/NetUtil.jsm");
 56 
 57 Cu.import("resource://conversations/stdlib/misc.js");
 58 Cu.import("resource://conversations/stdlib/msgHdrUtils.js");
 59 Cu.import("resource://conversations/log.js");
 60 
 61 let Log = setupLogging("Conversations.Compose");
 62 
 63 /**
 64  * Use the mailnews component to stream a message, and process it in a way
 65  *  that's suitable for quoting (strip signature, remove images, stuff like
 66  *  that).
 67  * @param {nsIMsgDBHdr} aMsgHdr The message header that you want to quote
 68  * @param {Function} k The continuation. This function will be passed quoted
 69  *  text suitable for insertion in a plaintext editor. The text must be appended
 70  *  to the mail body "as is", it shouldn't be run again through htmlToPlainText
 71  *  or whatever.
 72  * @return
 73  */
 74 function quoteMsgHdr(aMsgHdr, k) {
 75   let chunks = [];
 76   let listener = {
 77     /**@ignore*/
 78     setMimeHeaders: function () {
 79     },
 80 
 81     /**@ignore*/
 82     onStartRequest: function (/* nsIRequest */ aRequest, /* nsISupports */ aContext) {
 83     },
 84 
 85     /**@ignore*/
 86     onStopRequest: function (/* nsIRequest */ aRequest, /* nsISupports */ aContext, /* int */ aStatusCode) {
 87       let data = chunks.join("");
 88       k(htmlToPlainText(data));
 89     },
 90 
 91     /**@ignore*/
 92     onDataAvailable: function (/* nsIRequest */ aRequest, /* nsISupports */ aContext,
 93         /* nsIInputStream */ aStream, /* int */ aOffset, /* int */ aCount) {
 94       // Fortunately, we have in Gecko 2.0 a nice wrapper
 95       let data = NetUtil.readInputStreamToString(aStream, aCount);
 96       // Now each character of the string is actually to be understood as a byte
 97       //  of a UTF-8 string.
 98       // Everyone knows that nsICharsetConverterManager and nsIUnicodeDecoder
 99       //  are not to be used from scriptable code, right? And the error you'll
100       //  get if you try to do so is really meaningful, and that you'll have no
101       //  trouble figuring out where the error comes from...
102       let unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
103                              .createInstance(Ci.nsIScriptableUnicodeConverter);
104       unicodeConverter.charset = "UTF-8";
105       // So charCodeAt is what we want here...
106       let array = [];
107       for (let i = 0; i < data.length; ++i)
108         array[i] = data.charCodeAt(i);
109       // Yay, good to go!
110       chunks.push(unicodeConverter.convertFromByteArray(array, array.length));
111     },
112 
113     QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIStreamListener,
114       Ci.nsIMsgQuotingOutputStreamListener, Ci.nsIRequestObserver])
115   };
116   // Here's what we want to stream...
117   let msgUri = msgHdrGetUri(aMsgHdr);
118   /**
119    * Quote a particular message specified by its URI.
120    *
121    * @param charset optional parameter - if set, force the message to be
122    *                quoted using this particular charset
123    */
124   //   void quoteMessage(in string msgURI, in boolean quoteHeaders,
125   //                     in nsIMsgQuotingOutputStreamListener streamListener,
126   //                     in string charset, in boolean headersOnly);
127   let quoter = Cc["@mozilla.org/messengercompose/quoting;1"]
128                .createInstance(Ci.nsIMsgQuote);
129   quoter.quoteMessage(msgUri, false, listener, "", false);
130 }
131 
132 /**
133  * A function that properly quotes a plaintext email.
134  * @param {String} aStr The mail body that we're expected to quote.
135  * @return {String} The quoted mail body with >'s properly taken care of.
136  */
137 function citeString(aStr) {
138   let l = aStr.length;
139   return aStr.replace("\n", function (match, offset, str) {
140     // http://mxr.mozilla.org/comm-central/source/mozilla/editor/libeditor/text/nsInternetCiter.cpp#96
141     if (offset < l) {
142       if (str[offset+1] != ">")
143         return "\n> ";
144       else
145         return "\n>";
146     }
147   }, "g");
148 }
149 
150 /**
151  * Wrap some text. Beware, that function doesn't do rewrapping, and only
152  *  operates on non-quoted lines. This is only useful in our very specific case
153  *  where the quoted lines have been properly wrapped for format=flowed already,
154  *  and the non-quoted lines are the only ones that need wrapping for
155  *  format=flowed.
156  * Beware, this function will treat all lines starting with >'s as quotations,
157  *  even user-inserted ones. We would need support from the editor to proceed
158  *  otherwise, and the current textarea doesn't provide this.
159  * This function, when breaking lines, will do space-stuffing per the RFC if
160  *  after the break the text starts with From or >.
161  * @param {String} txt The text that should be wrapped.
162  * @param {Number} width (optional) The width we should wrap to. Default to 72.
163  * @return {String} The text with non-quoted lines wrapped. This is suitable for
164  *  sending as format=flowed.
165  */
166 function simpleWrap(txt, width) {
167   if (!width)
168     width = 72;
169 
170   function maybeEscape(line) {
171     if (line.indexOf("From") === 0 || line.indexOf(">") === 0)
172       return (" " + line);
173     else
174       return line;
175   }
176 
177   function splitLongLine(soFar, remaining) {
178     if (remaining.length > width) {
179       let i = width - 1;
180       while (remaining[i] != " " && i > 0)
181         i--;
182       if (i > 0) {
183         // This includes the trailing space that indicates that we are wrapping
184         //  a long line with format=flowed.
185         soFar.push(maybeEscape(remaining.substring(0, i+1)));
186         return splitLongLine(soFar, remaining.substring(i+1, remaining.length));
187       } else {
188         let j = remaining.indexOf(" ");
189         if (j > 0) {
190           // Same remark.
191           soFar.push(maybeEscape(remaining.substring(0, j+1)));
192           return splitLongLine(soFar, remaining.substring(j+1, remaining.length));
193         } else {
194           // Make sure no one interprets this as a line continuation.
195           soFar.push(remaining.trimRight());
196           return soFar.join("\n");
197         }
198       }
199     } else {
200       // Same remark.
201       soFar.push(maybeEscape(remaining.trimRight()));
202       return soFar.join("\n");
203     }
204   }
205 
206   let lines = txt.split(/\r?\n/);
207 
208   for each (let [i, line] in Iterator(lines)) {
209     if (line.length > width && line[0] != ">")
210       lines[i] = splitLongLine([], line);
211   }
212   return lines.join("\n");
213 }
214 
215 /**
216  * Convert HTML into text/plain suitable for insertion right away in the mail
217  *  body. If there is text with >'s at the beginning of lines, these will be
218  *  space-stuffed, and the same goes for Froms. <blockquote>s will be converted
219  *  with the suitable >'s at the beginning of the line, and so on...
220  * This function also takes care of rewrapping at 72 characters, so your quoted
221  *  lines will be properly wrapped too. This means that you can add some text of
222  *  your own, and then pass this to simpleWrap, it should "just work" (unless
223  *  the user has edited a quoted line and made it longer than 990 characters, of
224  *  course).
225  * @param {String} aHtml A string containing the HTML that's to be converted.
226  * @return {String} A text/plain string suitable for insertion in a mail body.
227  */
228 function htmlToPlainText(aHtml) {
229   // Yes, this is ridiculous, we're instanciating composition fields just so
230   //  that they call ConvertBufPlainText for us. But ConvertBufToPlainText
231   //  really isn't easily scriptable, so...
232   let fields = Cc["@mozilla.org/messengercompose/composefields;1"]
233                   .createInstance(Ci.nsIMsgCompFields);
234   fields.body = aHtml;
235   fields.forcePlainText = true;
236   fields.ConvertBodyToPlainText();
237   return fields.body;
238 }
239 
240 /**
241  * @ignore
242  */
243 function citeLevel (line) {
244   let i;
245   for (i = 0; line[i] == ">" && i < line.length; ++i)
246     ; // nop
247   return i;
248 };
249 
250 /**
251  * Just try to convert quoted lines back to HTML markup (<blockquote>s).
252  * @param {String} txt
253  * @return {String}
254  */
255 function plainTextToHtml(txt) {
256   let lines = txt.split(/\r?\n/);
257   let newLines = [];
258   let level = 0;
259   for each (let [, line] in Iterator(lines)) {
260     let newLevel = citeLevel(line);
261     if (newLevel > level)
262       for (let i = level; i < newLevel; ++i)
263         newLines.push('<blockquote type="cite">');
264     if (newLevel < level)
265       for (let i = newLevel; i < level; ++i)
266         newLines.push('</blockquote>');
267     let newLine = line[newLevel] == " "
268       ? escapeHtml(line.substring(newLevel + 1, line.length))
269       : escapeHtml(line.substring(newLevel, line.length))
270     ;
271     newLines.push(newLine);
272     level = newLevel;
273   }
274   return newLines.join("\n");
275 }
276