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