This is just some static backup of the original site, don't expect every link to work!

source: modules/stdlib/compose.js @ cbe8c6

ng_0.9
Last change on this file since cbe8c6 was cbe8c6, checked in by rene <rene@…>, 6 years ago

add stdlib

  • Property mode set to 100644
File size: 17.7 KB
Line 
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
43var EXPORTED_SYMBOLS = [
44  'quoteMsgHdr', 'citeString',
45  'htmlToPlainText', 'simpleWrap',
46  'plainTextToHtml', 'replyAllParams',
47  'determineComposeHtml', 'composeMessageTo',
48  'getSignatureContentsForAccount',
49]
50
51const {
52  classes: Cc,
53  interfaces: Ci,
54  utils: Cu,
55  results: Cr
56} = Components;
57
58Cu.import("resource://gre/modules/XPCOMUtils.jsm"); // for generateQI, defineLazyServiceGetter
59Cu.import("resource://gre/modules/NetUtil.jsm");
60Cu.import("resource:///modules/gloda/mimemsg.js");
61Cu.import("resource:///modules/mailServices.js");
62
63XPCOMUtils.importRelative(this, "misc.js");
64XPCOMUtils.importRelative(this, "msgHdrUtils.js");
65XPCOMUtils.importRelative(this, "../log.js");
66
67let Log = setupLogging(logRoot + ".Stdlib");
68
69/**
70 * Use the mailnews component to stream a message, and process it in a way
71 *  that's suitable for quoting (strip signature, remove images, stuff like
72 *  that).
73 * @param {nsIMsgDBHdr} aMsgHdr The message header that you want to quote
74 * @param {Function} k The continuation. This function will be passed quoted
75 *  text suitable for insertion in an HTML editor. You can pass this to
76 *  htmlToPlainText if you're running a plaintext editor.
77 * @return
78 */
79function quoteMsgHdr(aMsgHdr, k) {
80  let chunks = [];
81  // Everyone knows that nsICharsetConverterManager and nsIUnicodeDecoder
82  //  are not to be used from scriptable code, right? And the error you'll
83  //  get if you try to do so is really meaningful, and that you'll have no
84  //  trouble figuring out where the error comes from...
85  let unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
86    .createInstance(Ci.nsIScriptableUnicodeConverter);
87  unicodeConverter.charset = "UTF-8";
88  let listener = {
89    /**@ignore*/
90    setMimeHeaders: function () {},
91
92    /**@ignore*/
93    onStartRequest: function ( /* nsIRequest */ aRequest, /* nsISupports */ aContext) {},
94
95    /**@ignore*/
96    onStopRequest: function ( /* nsIRequest */ aRequest, /* nsISupports */ aContext, /* int */ aStatusCode) {
97      let data = chunks.join("");
98      k(data);
99    },
100
101    /**@ignore*/
102    onDataAvailable: function ( /* nsIRequest */ aRequest, /* nsISupports */ aContext,
103      /* nsIInputStream */
104      aStream, /* int */ aOffset, /* int */ aCount) {
105      // Fortunately, we have in Gecko 2.0 a nice wrapper
106      let data = NetUtil.readInputStreamToString(aStream, aCount);
107      // Now each character of the string is actually to be understood as a byte
108      //  of a UTF-8 string.
109      // So charCodeAt is what we want here...
110      let array = [];
111      for (let i = 0; i < data.length; ++i)
112        array[i] = data.charCodeAt(i);
113      // Yay, good to go!
114      chunks.push(unicodeConverter.convertFromByteArray(array, array.length));
115    },
116
117    QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIStreamListener,
118      Ci.nsIMsgQuotingOutputStreamListener, Ci.nsIRequestObserver
119    ])
120  };
121  // Here's what we want to stream...
122  let msgUri = msgHdrGetUri(aMsgHdr);
123  /**
124   * Quote a particular message specified by its URI.
125   *
126   * @param charset optional parameter - if set, force the message to be
127   *                quoted using this particular charset
128   */
129  //   void quoteMessage(in string msgURI, in boolean quoteHeaders,
130  //                     in nsIMsgQuotingOutputStreamListener streamListener,
131  //                     in string charset, in boolean headersOnly);
132  let quoter = Cc["@mozilla.org/messengercompose/quoting;1"]
133    .createInstance(Ci.nsIMsgQuote);
134  try {
135    quoter.quoteMessage(msgUri, false, listener, "", false);
136  } catch (e
137    if e.result == Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS) {
138    quoter.quoteMessage(msgUri, false, listener, "", false, aMsgHdr);
139  }
140}
141
142/**
143 * A function that properly quotes a plaintext email.
144 * @param {String} aStr The mail body that we're expected to quote.
145 * @return {String} The quoted mail body with &gt;'s properly taken care of.
146 */
147function citeString(aStr) {
148  let l = aStr.length;
149  return aStr.replace("\n", function (match, offset, str) {
150    // http://mxr.mozilla.org/comm-central/source/mozilla/editor/libeditor/text/nsInternetCiter.cpp#96
151    if (offset < l - 1) {
152      if (str[offset + 1] != ">" && str[offset + 1] != "\n" && str[offset + 1] != "\r")
153        return "\n> ";
154      else
155        return "\n>";
156    } else {
157      return match;
158    }
159  }, "g");
160}
161
162/**
163 * Wrap some text. Beware, that function doesn't do rewrapping, and only
164 *  operates on non-quoted lines. This is only useful in our very specific case
165 *  where the quoted lines have been properly wrapped for format=flowed already,
166 *  and the non-quoted lines are the only ones that need wrapping for
167 *  format=flowed.
168 * Beware, this function will treat all lines starting with >'s as quotations,
169 *  even user-inserted ones. We would need support from the editor to proceed
170 *  otherwise, and the current textarea doesn't provide this.
171 * This function, when breaking lines, will do space-stuffing per the RFC if
172 *  after the break the text starts with From or &gt;.
173 * @param {String} txt The text that should be wrapped.
174 * @param {Number} width (optional) The width we should wrap to. Default to 72.
175 * @return {String} The text with non-quoted lines wrapped. This is suitable for
176 *  sending as format=flowed.
177 */
178function simpleWrap(txt, width) {
179  if (!width)
180    width = 72;
181
182  function maybeEscape(line) {
183    if (line.indexOf("From") === 0 || line.indexOf(">") === 0)
184      return (" " + line);
185    else
186      return line;
187  }
188
189  /**
190   * That function takes a (long) line, and splits it into many lines.
191   * @param soFar {Array String} an accumulator of the lines we've wrapped already
192   * @param remaining {String} the remaining string to wrap
193   */
194  function splitLongLine(soFar, remaining) {
195    if (remaining.length > width) {
196      // Start at the end of the line, and move back until we find a word
197      // boundary.
198      let i = width - 1;
199      while (remaining[i] != " " && i > 0)
200        i--;
201      // We found a word boundary, break there
202      if (i > 0) {
203        // This includes the trailing space that indicates that we are wrapping
204        //  a long line with format=flowed.
205        soFar.push(maybeEscape(remaining.substring(0, i + 1)));
206        return splitLongLine(soFar, remaining.substring(i + 1, remaining.length));
207      } else {
208        // No word boundary, break at the first space
209        let j = remaining.indexOf(" ");
210        if (j > 0) {
211          // Same remark about the trailing space.
212          soFar.push(maybeEscape(remaining.substring(0, j + 1)));
213          return splitLongLine(soFar, remaining.substring(j + 1, remaining.length));
214        } else {
215          // Make sure no one interprets this as a line continuation.
216          soFar.push(remaining.trimRight());
217          return soFar.join("\n");
218        }
219      }
220    } else {
221      // Same remark about the trailing space.
222      soFar.push(maybeEscape(remaining.trimRight()));
223      return soFar.join("\n");
224    }
225  }
226
227  let lines = txt.split(/\r?\n/);
228
229  for each(let [i, line] in Iterator(lines)) {
230    if (line.length > width && line[0] != ">")
231      lines[i] = splitLongLine([], line);
232  }
233  return lines.join("\n");
234}
235
236/**
237 * Convert HTML into text/plain suitable for insertion right away in the mail
238 *  body. If there is text with &gt;'s at the beginning of lines, these will be
239 *  space-stuffed, and the same goes for Froms. &lt;blockquote&gt;s will be converted
240 *  with the suitable &gt;'s at the beginning of the line, and so on...
241 * This function also takes care of rewrapping at 72 characters, so your quoted
242 *  lines will be properly wrapped too. This means that you can add some text of
243 *  your own, and then pass this to simpleWrap, it should "just work" (unless
244 *  the user has edited a quoted line and made it longer than 990 characters, of
245 *  course).
246 * @param {String} aHtml A string containing the HTML that's to be converted.
247 * @return {String} A text/plain string suitable for insertion in a mail body.
248 */
249function htmlToPlainText(aHtml) {
250  // Yes, this is ridiculous, we're instanciating composition fields just so
251  //  that they call ConvertBufPlainText for us. But ConvertBufToPlainText
252  //  really isn't easily scriptable, so...
253  let fields = Cc["@mozilla.org/messengercompose/composefields;1"]
254    .createInstance(Ci.nsIMsgCompFields);
255  fields.body = aHtml;
256  fields.forcePlainText = true;
257  fields.ConvertBodyToPlainText();
258  return fields.body;
259}
260
261/**
262 * @ignore
263 */
264function citeLevel(line) {
265  let i;
266  for (i = 0; line[i] == ">" && i < line.length; ++i)
267  ; // nop
268  return i;
269};
270
271/**
272 * Just try to convert quoted lines back to HTML markup (&lt;blockquote&gt;s).
273 * @param {String} txt
274 * @return {String}
275 */
276function plainTextToHtml(txt) {
277  let lines = txt.split(/\r?\n/);
278  let newLines = [];
279  let level = 0;
280  for each(let [, line] in Iterator(lines)) {
281    let newLevel = citeLevel(line);
282    if (newLevel > level)
283      for (let i = level; i < newLevel; ++i)
284        newLines.push('<blockquote type="cite">');
285    if (newLevel < level)
286      for (let i = newLevel; i < level; ++i)
287        newLines.push('</blockquote>');
288    let newLine = line[newLevel] == " " ? escapeHtml(line.substring(newLevel + 1, line.length)) : escapeHtml(line.substring(newLevel, line.length));
289    newLines.push(newLine);
290    level = newLevel;
291  }
292  return newLines.join("\n");
293}
294
295function parse(aMimeLine) {
296  let emails = {};
297  let fullNames = {};
298  let names = {};
299  let numAddresses = MailServices.headerParser.parseHeadersWithArray(aMimeLine, emails, names, fullNames);
300  return [names.value, emails.value];
301}
302
303/**
304 * Analyze a message header, and then return all the compose parameters for the
305 * reply-all case.
306 * @param {nsIIdentity} The identity you've picked for the reply.
307 * @param {nsIMsgDbHdr} The message header.
308 * @param {k} The function to call once we've determined all parameters. Take an
309 *  argument like
310 *  {{ to: [[name, email]], cc: [[name, email]], bcc: [[name, email]]}}
311 */
312function replyAllParams(aIdentity, aMsgHdr, k) {
313  // Do the whole shebang to find out who to send to...
314  let [
315    [author],
316    [authorEmailAddress]
317  ] = parse(aMsgHdr.author);
318  let [recipients, recipientsEmailAddresses] = parse(aMsgHdr.recipients);
319  let [ccList, ccListEmailAddresses] = parse(aMsgHdr.ccList);
320  let [bccList, bccListEmailAddresses] = parse(aMsgHdr.bccList);
321  authorEmailAddress = authorEmailAddress.toLowerCase();
322  recipientsEmailAddresses = [x.toLowerCase()
323    for each([, x] in Iterator(recipientsEmailAddresses))
324  ];
325  ccListEmailAddresses = [x.toLowerCase() for each([, x] in Iterator(ccListEmailAddresses))];
326  bccListEmailAddresses = [x.toLowerCase() for each([, x] in Iterator(bccListEmailAddresses))];
327  let identity = aIdentity;
328  let identityEmail = identity.email.toLowerCase();
329  let to = [],
330    cc = [],
331    bcc = [];
332
333  let isReplyToOwnMsg = false;
334  for each(let [i, identity] in Iterator(gIdentities)) {
335    // It happens that gIdentities.default is null!
336    if (!identity) {
337      Log.debug("This identity is null, pretty weird...");
338      continue;
339    }
340    let email = identity.email.toLowerCase();
341    if (email == authorEmailAddress)
342      isReplyToOwnMsg = true;
343    if (recipientsEmailAddresses.some(function (x) x == email))
344      isReplyToOwnMsg = false;
345    if (ccListEmailAddresses.some(function (x) x == email))
346      isReplyToOwnMsg = false;
347  }
348
349  // Actually we are implementing the "Reply all" logic... that's better, no one
350  //  wants to really use reply anyway ;-)
351  if (isReplyToOwnMsg) {
352    to = [
353      [r, recipientsEmailAddresses[i]]
354      for each([i, r] in Iterator(recipients))
355    ];
356  } else {
357    to = [
358      [author, authorEmailAddress]
359    ];
360  }
361  cc = [
362    [cc, ccListEmailAddresses[i]]
363    for each([i, cc] in Iterator(ccList))
364    if (ccListEmailAddresses[i] != identityEmail)
365  ];
366  if (!isReplyToOwnMsg)
367    cc = cc.concat([
368      [r, recipientsEmailAddresses[i]]
369      for each([i, r] in Iterator(recipients))
370      if (recipientsEmailAddresses[i] != identityEmail)
371    ]);
372  bcc = [
373    [bcc, bccListEmailAddresses[i]]
374    for each([i, bcc] in Iterator(bccList))
375  ];
376
377  let finish = function (to, cc, bcc) {
378    let hashMap = {};
379    for each(let [name, email] in to)
380    hashMap[email] = null;
381    cc = cc.filter(function ([name, email]) let (r = (email in hashMap))
382      (hashMap[email] = null, !r)
383    );
384    bcc = bcc.filter(function ([name, email]) let (r = (email in hashMap))
385      (hashMap[email] = null, !r)
386    );
387    k({
388      to: to,
389      cc: cc,
390      bcc: bcc
391    });
392  }
393
394  // Do we have a Reply-To header?
395  msgHdrGetHeaders(aMsgHdr, function (aHeaders) {
396    if (aHeaders.has("reply-to")) {
397      let [names, emails] = parse(aHeaders.get("reply-to"));
398      emails = [email.toLowerCase() for each(email in emails)];
399      if (emails.length) {
400        // Invariant: at this stage, we only have one item in to.
401        cc = cc.concat([to[0]]); // move the to in cc
402        to = combine(names, emails);
403      }
404    }
405    finish(to, cc, bcc);
406  });
407}
408
409/**
410 * This function replaces nsMsgComposeService::determineComposeHTML, which is
411 * marked as [noscript], just to make our lives complicated. [insert random rant
412 * here].
413 *
414 * @param aIdentity (optional) You can specify the identity which you would like
415 * to get the preference for.
416 * @return a bool which is true if you should compose in HTML
417 */
418function determineComposeHtml(aIdentity) {
419  if (!aIdentity)
420    aIdentity = gIdentities.default;
421
422  if (aIdentity) {
423    return (aIdentity.composeHtml == Ci.nsIMsgCompFormat.HTML);
424  } else {
425    return Cc["@mozilla.org/preferences-service;1"]
426      .getService(Ci.nsIPrefService)
427      .getBranch(null)
428      .getBoolPref("mail.compose_html");
429  }
430}
431
432/**
433 * Open a composition window for the given email address.
434 * @param aEmail {String}
435 * @param aDisplayedFolder {nsIMsgFolder} pass gFolderDisplay.displayedFolder
436 */
437function composeMessageTo(aEmail, aDisplayedFolder) {
438  let fields = Cc["@mozilla.org/messengercompose/composefields;1"]
439    .createInstance(Ci.nsIMsgCompFields);
440  let params = Cc["@mozilla.org/messengercompose/composeparams;1"]
441    .createInstance(Ci.nsIMsgComposeParams);
442  fields.to = aEmail
443  params.type = Ci.nsIMsgCompType.New;
444  params.format = Ci.nsIMsgCompFormat.Default;
445  if (aDisplayedFolder) {
446    params.identity = MailServices.accounts
447      .getFirstIdentityForServer(aDisplayedFolder.server);
448  }
449  params.composeFields = fields;
450  MailServices.compose.OpenComposeWindowWithParams(null, params);
451}
452
453/**
454 * Returns signature contents depending on account settings of the identity.
455 * HTML signature is converted to plain text.
456 * @param {nsIIdentity} The identity you've picked for the reply.
457 * @return {String} plain text signature
458 */
459function getSignatureContentsForAccount(aIdentity) {
460  let signature = "";
461  if (!aIdentity)
462    return signature;
463
464  if (aIdentity.attachSignature && aIdentity.signature) {
465    let charset = systemCharset();
466    const replacementChar =
467      Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER;
468    let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
469      .createInstance(Ci.nsIFileInputStream);
470    let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
471      .createInstance(Ci.nsIConverterInputStream);
472    try {
473      fstream.init(aIdentity.signature, -1, 0, 0);
474      try {
475        cstream.init(fstream, charset, 1024, replacementChar);
476      } catch (e) {
477        Log.error("ConverterInputStream init error: " + e +
478          "\n charset: " + charset + "\n");
479        cstream.init(fstream, "UTF-8", 1024, replacementChar);
480      }
481      let str = {};
482      while (cstream.readString(4096, str) != 0) {
483        signature += str.value;
484      }
485      if (aIdentity.signature.path.match(/\.html?$/)) {
486        signature = htmlToPlainText(signature);
487      }
488    } catch (e) {
489      Log.error("Signature file stream error: " + e + "\n");
490    }
491    cstream.close();
492    fstream.close();
493    // required for stripSignatureIfNeeded working properly
494    signature = signature.replace(/\r?\n/g, "\n");
495  } else {
496    signature = aIdentity.htmlSigFormat ? htmlToPlainText(aIdentity.htmlSigText) : aIdentity.htmlSigText;
497  }
498  return signature;
499}
Note: See TracBrowser for help on using the repository browser.