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