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 This file provides a Javascript abstraction for sending a
 39  * message.
 40  * @author Jonathan Protzenko
 41  */
 42 
 43 var EXPORTED_SYMBOLS = ['sendMessage']
 44 
 45 const Ci = Components.interfaces;
 46 const Cc = Components.classes;
 47 const Cu = Components.utils;
 48 const Cr = Components.results;
 49 
 50 Cu.import("resource://gre/modules/PluralForm.jsm");
 51 Cu.import("resource:///modules/MailUtils.js"); // for getFolderForURI
 52 
 53 const gHeaderParser = Cc["@mozilla.org/messenger/headerparser;1"]
 54                       .getService(Ci.nsIMsgHeaderParser);
 55 const msgComposeService = Cc["@mozilla.org/messengercompose;1"]
 56                           .getService(Ci.nsIMsgComposeService);
 57 const mCompType = Ci.nsIMsgCompType;
 58 
 59 Cu.import("resource://conversations/stdlib/misc.js");
 60 Cu.import("resource://conversations/stdlib/msgHdrUtils.js");
 61 Cu.import("resource://conversations/stdlib/compose.js");
 62 Cu.import("resource://conversations/log.js");
 63 
 64 let Log = setupLogging("Conversations.Send");
 65 
 66 /**
 67  * Get the Archive folder URI depending on the given identity and the given Date
 68  *  object.
 69  * @param {nsIMsgIdentity} identity
 70  * @param {Date} msgDate
 71  * @return {String} The URI for the folder. Use MailUtils.getFolderForURI.
 72  */
 73 function getArchiveFolderUriFor(identity, msgDate) {
 74   let msgYear = msgDate.getFullYear().toString();
 75   let monthFolderName = msgDate.toLocaleFormat("%Y-%m");
 76   let granularity = identity.archiveGranularity;
 77   let folderUri = identity.archiveFolder;
 78   if (granularity >= Ci.nsIMsgIdentity.perYearArchiveFolders)
 79     folderUri += "/" + msgYear;
 80   if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders)
 81     folderUri += "/" + monthFolderName;
 82   return folderUri;
 83 }
 84 
 85 // This has to be a root because once the msgCompose has deferred the treatment
 86 //  of the send process to nsMsgSend.cpp, the nsMsgSend holds a reference to
 87 //  nsMsgCopySendListener (nsMsgCompose.cpp). nsMsgCopySendListener holds a
 88 //  *weak* reference to its corresponding nsIMsgCompose object, that in turns
 89 //  forwards the notifications to our own little progressListener.
 90 // So if no one holds a firm reference to gMsgCompose, then it might end up
 91 //  being collected before the send process terminates, and then, it's BAD.
 92 // The bad case would be:
 93 //  * user hits "send"
 94 //  * quickly changes conversations
 95 //  * writes a new email
 96 //  * the previous send hasn't completed, but the user hits send anyway
 97 //  * gMsgCompose is overridden
 98 //  * a garbage collection kicks in, collects the previous StateListener
 99 //  * first send completes
100 //  * the first listener fails to receive the notification.
101 // That's way too implausible, so I'll just assume this doesn't happen!
102 let gMsgCompose;
103 
104 /**
105  * This is our monstrous Javascript function for sending a message. It hides all
106  *  the atrocities of nsMsgCompose.cpp and nsMsgSend.cpp for you, and it
107  *  provides what I hope is a much more understandable interface.
108  * You are expected to provide the whole set of listeners. The most interesting
109  *  one is the stateListener, since it has the ComposeProcessDone notification.
110  * This version only does plaintext composition but I hope to enhance it with
111  *  both HTML and plaintext in the future.
112  * @param composeParameters
113  * @param composeParameters.identity The identity the user picked to send the
114  *  message
115  * @param composeParameters.to The recipients. This is a comma-separated list of
116  *  valid email addresses that must be escaped already. You probably want to use
117  *  nsIMsgHeaderParser.MakeFullAddress to deal with names that contain commas.
118  * @param composeParameters.cc Same remark.
119  * @param composeParameters.bcc Same remark.
120  * @param composeParameters.subject The subject, no restrictions on that one.
121  *
122  * @param sendingParameters
123  * @param sendingParameters.deliverType See Ci.nsIMsgCompDeliverMode
124  * @param sendingParameters.compType See Ci.nsIMsgCompType. We use this to
125  *  determine what kind of headers we should set (Reply-To, References...).
126  *
127  * @param aNode The DOM node that holds the editing session. Right now, it's
128  *  kinda useless if it's only plaintext, but it's relevant for the HTML
129  *  composition (because nsMsgSend queries the original DOM node to find out
130  *  about inline images).
131  *
132  * @param listeners
133  * @param listeners.progressListener That one monitors the progress of long
134  *  operations (like sending a message with attachments), it's notified with the
135  *  current percentage of completion.
136  * @param listeners.sendListener That one receives notifications about factual
137  *  events (sending, copying to Sent, ...). It receives notifications with
138  *  statuses.
139  * @param listeners.stateListener This one is a high-level listener that
140  *   receives notifications about the global composition process.
141  *
142  * @param options
143  * @param options.popOut Don't send the message, just transfer it to a new
144  *  composition window.
145  * @param options.archive Shall we archive the message right away? This won't
146  *  even copy it to the Sent folder. Warning: this one assumes that the "right"
147  *  Archives folder already exists.
148  */
149 function sendMessage({ msgHdr, identity, to, cc, bcc, subject },
150     { deliverType, compType },
151     aNode,
152     { progressListener, sendListener, stateListener },
153     { popOut, archive }) {
154 
155   // Here is the part where we do all the stuff related to filling proper
156   //  headers, adding references, making sure all the composition fields are
157   //  properly set before assembling the message.
158   let fields = Cc["@mozilla.org/messengercompose/composefields;1"]
159                   .createInstance(Ci.nsIMsgCompFields);
160   fields.from = gHeaderParser.makeFullAddress(identity.fullName, identity.email);
161   fields.to = to;
162   fields.cc = cc;
163   fields.bcc = bcc;
164   fields.subject = subject;
165 
166   let references = [];
167   switch (compType) {
168     case mCompType.New:
169       break;
170 
171     case mCompType.Reply:
172     case mCompType.ReplyAll:
173     case mCompType.ReplyToSender:
174     case mCompType.ReplyToGroup:
175     case mCompType.ReplyToSenderAndGroup:
176     case mCompType.ReplyWithTemplate:
177     case mCompType.ReplyToList:
178       references = [msgHdr.getStringReference(i)
179         for each (i in range(0, msgHdr.numReferences))];
180       references.push(msgHdr.messageId);
181       break;
182 
183     case mCompType.ForwardAsAttachment:
184     case mCompType.ForwardInline:
185       references.push(msgHdr.messageId);
186       break;
187   }
188   references = ["<"+x+">" for each ([, x] in Iterator(references))];
189   fields.references = references.join(" ");
190 
191   // TODO:
192   // - fields.addAttachment (when attachments taken into account)
193 
194   // See suite/mailnews/compose/MsgComposeCommands.js#1783
195   // We're explicitly forcing plaintext here. SendMsg is thought-out well enough
196   //  and checks whether we're composing html. If we're not, it uses either the
197   //  contents of the nsPlainTextEditor::OutputToString if we have an editor, or
198   //  the original contents of the fields if we have no editor. That suits us
199   //  well.
200   // http://mxr.mozilla.org/comm-central/source/mailnews/compose/src/nsMsgCompose.cpp#1102
201   // 
202   // What we could do (better) is call msgCompose.InitEditor with a fake
203   //  plaintext editor that implements nsIMailEditorSupport and has an
204   //  OutputToString method.  We would also lift the requirement on
205   //  forcePlainText, and allow multipart/alternative, which would result in the
206   //  mozITXTToHTMLConv being run to convert *bold* to <b>bold</b> and so on.
207   // Please note that querying the editor for its contents is the responsibility
208   //  of nsMsgSend.
209   // http://mxr.mozilla.org/comm-central/source/mailnews/compose/src/nsMsgSend.cpp#1615
210   //
211   // See also nsMsgSend:620 for a vague explanation on how the editor's HTML
212   //  ends up being converted as text/plain, for the case where we would like to
213   //  offer HTML editing.
214   fields.useMultipartAlternative = false;
215   // We're in 2011 now, let's assume everyone knows how to read UTF-8
216   fields.bodyIsAsciiOnly = false;
217   fields.characterSet = "UTF-8";
218   fields.body = aNode.value+"\n"; // Doesn't work without the newline. Weird. IMAP stuff.
219 
220   // If we are to archive the conversation after sending, this means we also
221   //  have to archive the sent message as well. The simple way to do it is to
222   //  change the FCC (Folder CC) from the Sent folder to the Archives folder.
223   if (archive) {
224     // We're just assuming that the folder exists, this might not be the case...
225     // But I am so NOT reimplementing the whole logic from
226     //  http://mxr.mozilla.org/comm-central/source/mail/base/content/mailWindowOverlay.js#1293
227     let folderUri = getArchiveFolderUriFor(identity, new Date());
228     if (MailUtils.getFolderForURI(folderUri, true)) {
229       Log.debug("Message will be copied in", folderUri, "once sent");
230       fields.fcc = folderUri;
231     } else {
232       Log.warn("The archive folder doesn't exist yet, so the last message you sent won't be archived... sorry!");
233     }
234   }
235 
236   // We init the composition service with the right parameters, and we make sure
237   //  we're announcing that we're about to compose in plaintext, so that it
238   //  doesn't assume anything about having an editor (composing HTML implies
239   //  having an editor instance for the compose service).
240   // The variable we're interested in is m_composeHTML in nsMsgCompose.cpp – its
241   //  initial value is PR_FALSE. The idea is that the msgComposeFields serve
242   //  different purposes:
243   //  - they initially represent the initial parameters to setup the compose
244   //  window and,
245   //  - once the composition is done, they represent the compose session that
246   //  just finished (one notable exception is that if the editor is composing
247   //  HTML, fields.body is irrelevant and the SendMsg code will query the editor
248   //  for its HTML and/or plaintext contents).
249   // The value is to be updated depending on the account's settings to determine
250   //  whether we want HTML composition or not. This is nsMsgCompose::Initialize.
251   //  Well, guess what? We're not calling that function, and we make sure
252   //  m_composeHTML stays PR_FALSE until the end!
253   let params = Cc["@mozilla.org/messengercompose/composeparams;1"]
254                   .createInstance(Ci.nsIMsgComposeParams);
255   params.composeFields = fields;
256   params.identity = identity;
257   params.type = compType;
258   params.sendListener = sendListener;
259 
260   // If we want to switch to the external editor, we assembled all the
261   //  composition fields properly. Pass them to a compose window, and move on.
262   if (popOut) {
263     // We set all the fields ourselves, force New so that the compose code
264     //  doesn't try to figure out the parameters by itself.
265     // XXX maybe we should just use New everywhere since we're setting the
266     //  parameters ourselves anyway...
267     fields.characterSet = "UTF-8";
268     fields.forcePlainText = false;
269     // If we don't do that the editor compose window will think that the >s that
270     //  are inserted by the user are voluntary, that is, they should be escaped
271     //  so that they are not parsed as quotes. We don't want that!
272     // The best solution is to fire the HTML editor and replace the cited lines
273     //  by the appropriate blockquotes.
274     // XXX please note that we are not trying to preserve spacing, or stuff like
275     //  that -- they'll die in the translation. So ASCII art quoted in the quick
276     //  reply won't be preserved. We also won't preserve the format=flowed
277     //  thing: if we were to do the right thing (tm) we would unparse the quoted
278     //  lines and push them as single lines in the HTML, with no <br>s in the
279     //  middle, but well... I guess this is okay enough.
280     fields.body = plainTextToHtml(fields.body);
281 
282     params.format = Ci.nsIMsgCompFormat.HTML;
283     params.type = mCompType.New;
284     msgComposeService.OpenComposeWindowWithParams(null, params);
285     return true;
286   } else {
287     fields.forcePlainText = true;
288     // So we should have something more elaborate than a simple textarea. The
289     //  reason is, we should be able to differentiate between user-inserted >'s
290     //  and quote-inserted >'s. (The standard Thunderbird plaintext editor does
291     //  it with a blue color). The user-inserted >'s want a space prepended so
292     //  that the MUA doesn't interpret them as quotation. Real quotations don't.
293     // This is kinda out of scope so we're leaving the issue non-fixed but this
294     //  is clearly a FIXME.
295     fields.body = simpleWrap(fields.body, 72);
296     params.format = Ci.nsIMsgCompFormat.PlainText;
297 
298     // This part initializes a nsIMsgCompose instance. This is useless, because
299     //  that component is supposed to talk to the "real" compose window, set the
300     //  encoding, set the composition mode... we're only doing that because we
301     //  can't send the message ourselves because of too many [noscript]s.
302     if ("InitCompose" in msgComposeService) // comm-1.9.2
303       gMsgCompose = msgComposeService.InitCompose (null, params);
304     else // comm-central
305       gMsgCompose = msgComposeService.initCompose(params);
306 
307     // We create a progress listener...
308     var progress = Cc["@mozilla.org/messenger/progress;1"]
309                      .createInstance(Ci.nsIMsgProgress);
310     if (progress) {
311       progress.registerListener(progressListener);
312     }
313     gMsgCompose.RegisterStateListener(stateListener);
314 
315     try {
316       gMsgCompose.SendMsg(deliverType, identity, "", null, progress);
317     } catch (e) {
318       Log.error(e);
319       dumpCallStack(e);
320     }
321     return true;
322   }
323 }
324