forked from protz/thunderbird-stdlib
-
Notifications
You must be signed in to change notification settings - Fork 0
/
send.js
464 lines (429 loc) · 18.9 KB
/
send.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Thunderbird Conversations
*
* The Initial Developer of the Original Code is
* Jonathan Protzenko
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/**
* @fileoverview This file provides a Javascript abstraction for sending a
* message.
* @author Jonathan Protzenko
*/
var EXPORTED_SYMBOLS = ['sendMessage']
const {classes: Cc, interfaces: Ci, utils: Cu, results : Cr} = Components;
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); // for generateQI, defineLazyServiceGetter
Cu.import("resource:///modules/MailUtils.js"); // for getFolderForURI
Cu.import("resource:///modules/mailServices.js");
const mCompType = Ci.nsIMsgCompType;
const isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
XPCOMUtils.importRelative(this, "misc.js");
XPCOMUtils.importRelative(this, "msgHdrUtils.js");
XPCOMUtils.importRelative(this, "compose.js");
XPCOMUtils.importRelative(this, "../log.js");
let Log = setupLogging(logRoot+".Send");
/**
* Get the Archive folder URI depending on the given identity and the given Date
* object.
* @param {nsIMsgIdentity} identity
* @param {Date} msgDate
* @return {String} The URI for the folder. Use MailUtils.getFolderForURI.
*/
function getArchiveFolderUriFor(identity, msgDate) {
let msgYear = msgDate.getFullYear().toString();
let monthFolderName = msgDate.toLocaleFormat("%Y-%m");
let granularity = identity.archiveGranularity;
let folderUri = identity.archiveFolder;
if (granularity >= Ci.nsIMsgIdentity.perYearArchiveFolders)
folderUri += "/" + msgYear;
if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders)
folderUri += "/" + monthFolderName;
return folderUri;
}
function wrapBody(t) {
let r =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n"+
"<html>\n"+
" <head>\n"+
" <meta http-equiv=\"content-type\" content=\"text/html;\n"+
" charset=ISO-8859-1\">\n"+
" </head>\n"+
" <body>"+
" "+t+"\n"+
" </body>\n"+
"</html>\n"
;
return r;
}
/**
* This is our fake editor. We manipulate carefully the functions from
* nsMsgCompose.cpp so that it just figures out we have an editor, but doesn't
* try to interact with it.
* We'll probably try to improve this in the near future.
*/
function FakeEditor (aIframe) {
this.iframe = aIframe;
}
FakeEditor.prototype = {
getEmbeddedObjects: function _FakeEditor_getEmbeddedObjects () {
try {
let objects = Cc["@mozilla.org/supports-array;1"]
.createInstance(Ci.nsISupportsArray);
for each (let [, o] in Iterator(this.iframe.contentDocument.getElementsByTagName("img")))
objects.AppendElement(o, false);
return objects;
} catch (e) {
Log.error(e);
dumpCallStack(e);
}
},
outputToString: function _FakeEditor_outputToString (formatType, flags) {
Log.debug("Returning mail body for", formatType);
let html = this.iframe.contentDocument.body.innerHTML;
switch (formatType) {
case "text/plain":
return htmlToPlainText(html)+"\n";
case "text/html":
return wrapBody(html);
default:
Log.error("Unexpected formatType", formatType, flags);
}
},
// bodyConvertible calls GetRootElement on m_editor which is supposed to be an
// nsIEditor, so implement this property...
get rootElement () {
return this.iframe.contentDocument.body;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIEditor, Ci.nsIEditorMailSupport]),
}
// This has to be a root because once the msgCompose has deferred the treatment
// of the send process to nsMsgSend.cpp, the nsMsgSend holds a reference to
// nsMsgCopySendListener (nsMsgCompose.cpp). nsMsgCopySendListener holds a
// *weak* reference to its corresponding nsIMsgCompose object, that in turns
// forwards the notifications to our own little progressListener.
// So if no one holds a firm reference to gMsgCompose, then it might end up
// being collected before the send process terminates, and then, it's BAD.
// The bad case would be:
// * user hits "send"
// * quickly changes conversations
// * writes a new email
// * the previous send hasn't completed, but the user hits send anyway
// * gMsgCompose is overridden
// * a garbage collection kicks in, collects the previous StateListener
// * first send completes
// * the first listener fails to receive the notification.
// That's way too implausible, so I'll just assume this doesn't happen!
let gMsgCompose;
function initCompose(aMsgComposeService, aParams, aWindow, aDocShell) {
if ("InitCompose" in aMsgComposeService) {
return aMsgComposeService.InitCompose(aWindow, aParams);
} else {
return aMsgComposeService.initCompose(aParams, aWindow, aDocShell);
}
}
/**
* This is our monstrous Javascript function for sending a message. It hides all
* the atrocities of nsMsgCompose.cpp and nsMsgSend.cpp for you, and it
* provides what I hope is a much more understandable interface.
* You are expected to provide the whole set of listeners. The most interesting
* one is the stateListener, since it has the ComposeProcessDone notification.
* This version only does plaintext composition but I hope to enhance it with
* both HTML and plaintext in the future.
* @param composeParameters
* @param composeParameters.urls {Array} A list of message URLs that are to be
* used for references.
* @param composeParameters.identity The identity the user picked to send the
* message
* @param composeParameters.to The recipients. This is a comma-separated list of
* valid email addresses that must be escaped already. You probably want to use
* nsIMsgHeaderParser.MakeFullAddress to deal with names that contain commas.
* @param composeParameters.cc (optional) Same remark.
* @param composeParameters.bcc (optional) Same remark.
* @param composeParameters.subject The subject, no restrictions on that one.
* @param composeParameters.attachments A list of nsIMsgAttachment objects
* (optional)
* @param composeParameters.returnReceipt (optional)
* @param composeParameters.receiptType (optional)
* @param composeParameters.requestDsn (optional)
* @param composeParameters.securityInfo (optional)
*
* @param sendingParameters
* @param sendingParameters.deliverType See Ci.nsIMsgCompDeliverMode
* @param sendingParameters.compType See Ci.nsIMsgCompType. We use this to
* determine what kind of headers we should set (Reply-To, References...).
*
* @param aBody A visitor pattern, again. Calls x.editor(iframe) if this is a
* fully-fledged html editing session, or calls x.plainText(body) if this is
* just a simple plaintext mail.
*
* @param listeners
* @param listeners.progressListener That one monitors the progress of long
* operations (like sending a message with attachments), it's notified with the
* current percentage of completion.
* @param listeners.sendListener That one receives notifications about factual
* events (sending, copying to Sent, ...). It receives notifications with
* statuses.
* @param listeners.stateListener This one is a high-level listener that
* receives notifications about the global composition process.
*
* @param options
* @param options.popOut Don't send the message, just transfer it to a new
* composition window.
* @param options.archive Shall we archive the message right away? This won't
* even copy it to the Sent folder. Warning: this one assumes that the "right"
* Archives folder already exists.
*/
function sendMessage(params,
{ deliverType, compType },
aBody,
{ progressListener, sendListener, stateListener },
options) {
let popOut = options && options.popOut;
let archive = options && options.archive;
let { urls, identity, to, subject } = params;
let attachments = params.attachments || [];
// Here is the part where we do all the stuff related to filling proper
// headers, adding references, making sure all the composition fields are
// properly set before assembling the message.
let fields = Cc["@mozilla.org/messengercompose/composefields;1"]
.createInstance(Ci.nsIMsgCompFields);
fields.from = MailServices.headerParser.makeFullAddress(identity.fullName, identity.email);
fields.to = to;
if ("cc" in params)
fields.cc = params.cc;
if ("bcc" in params)
fields.bcc = params.bcc;
fields.subject = subject;
fields.returnReceipt = ("returnReceipt" in params)
? params.returnReceipt
: identity.requestReturnReceipt;
fields.receiptHeaderType = ("receiptType" in params)
? params.receiptType
: identity.receiptHeaderType;
fields.DSN = ("requestDsn" in params)
? params.requestDsn
: identity.requestDSN;
if ("securityInfo" in params)
fields.securityInfo = params.securityInfo;
let references = [];
switch (compType) {
case mCompType.New:
break;
case mCompType.Draft: {
// Copy the references from the original draft into the message we're
// about to send...
Log.assert(urls.length == 1, "Can't edit more than one message at a time");
let msgHdr = msgUriToMsgHdr(urls[0]);
references = [msgHdr.getStringReference(i)
for each (i in range(0, msgHdr.numReferences))];
break;
}
case mCompType.Reply:
case mCompType.ReplyAll:
case mCompType.ReplyToSender:
case mCompType.ReplyToGroup:
case mCompType.ReplyToSenderAndGroup:
case mCompType.ReplyWithTemplate:
case mCompType.ReplyToList: {
Log.assert(urls.length == 1, "Can't reply to more than one message at a time");
let msgHdr = msgUriToMsgHdr(urls[0]);
references = [msgHdr.getStringReference(i)
for each (i in range(0, msgHdr.numReferences))];
references.push(msgHdr.messageId);
break;
}
case mCompType.ForwardAsAttachment: {
for each (let [, url] in Iterator(urls)) {
let msgHdr = msgUriToMsgHdr(url);
references.push(msgHdr.messageId);
}
break;
}
case mCompType.ForwardInline: {
Log.assert(urls.length == 1, "Can't forward inline more than one message at a time");
let msgHdr = msgUriToMsgHdr(urls[0]);
references.push(msgHdr.messageId);
break;
}
}
references = ["<"+x+">" for each ([, x] in Iterator(references))];
fields.references = references.join(" ");
[fields.addAttachment(x) for each ([, x] in Iterator(attachments))];
// If we are to archive the conversation after sending, this means we also
// have to archive the sent message as well. The simple way to do it is to
// change the FCC (Folder CC) from the Sent folder to the Archives folder.
if (archive) {
// We're just assuming that the folder exists, this might not be the case...
// But I am so NOT reimplementing the whole logic from
// http://mxr.mozilla.org/comm-central/source/mail/base/content/mailWindowOverlay.js#1293
let folderUri = getArchiveFolderUriFor(identity, new Date());
if (MailUtils.getFolderForURI(folderUri, true)) {
Log.debug("Message will be copied in", folderUri, "once sent");
fields.fcc = folderUri;
} else {
Log.warn("The archive folder doesn't exist yet, so the last message you sent won't be archived... sorry!");
}
}
// We init the composition service with the right parameters, and we make sure
// we're announcing that we're about to compose in plaintext, so that it
// doesn't assume anything about having an editor (composing HTML implies
// having an editor instance for the compose service).
// The variable we're interested in is m_composeHTML in nsMsgCompose.cpp – its
// initial value is PR_FALSE. The idea is that the msgComposeFields serve
// different purposes:
// - they initially represent the initial parameters to setup the compose
// window and,
// - once the composition is done, they represent the compose session that
// just finished (one notable exception is that if the editor is composing
// HTML, fields.body is irrelevant and the SendMsg code will query the editor
// for its HTML and/or plaintext contents).
// The value is to be updated depending on the account's settings to determine
// whether we want HTML composition or not. This is nsMsgCompose::Initialize.
// Well, guess what? We're not calling that function, and we make sure
// m_composeHTML stays PR_FALSE until the end!
let params = Cc["@mozilla.org/messengercompose/composeparams;1"]
.createInstance(Ci.nsIMsgComposeParams);
params.composeFields = fields;
params.identity = identity;
params.type = compType;
params.sendListener = sendListener;
// If we want to switch to the external editor, we assembled all the
// composition fields properly. Pass them to a compose window, and move on.
if (popOut) {
let composeHtml = determineComposeHtml(identity);
// We set all the fields ourselves, force New so that the compose code
// doesn't try to figure out the parameters by itself.
fields.characterSet = "UTF-8";
// If we don't do that the editor compose window will think that the >s that
// are inserted by the user are voluntary, that is, they should be escaped
// so that they are not parsed as quotes. We don't want that!
// The best solution is to fire the HTML editor and replace the cited lines
// by the appropriate blockquotes.
// XXX please note that we are not trying to preserve spacing, or stuff like
// that -- they'll die in the translation. So ASCII art quoted in the quick
// reply won't be preserved. We also won't preserve the format=flowed
// thing: if we were to do the right thing (tm) we would unparse the quoted
// lines and push them as single lines in the HTML, with no <br>s in the
// middle, but well... I guess this is okay enough.
aBody.match({
plainText: function (body) {
if (composeHtml)
fields.body = plainTextToHtml(body);
else
fields.body = body;
},
editor: function (iframe) {
let html = iframe.contentDocument.body.innerHTML;
if (composeHtml)
fields.body = html;
else
fields.body = htmlToPlainText(html);
},
});
params.format = composeHtml
? Ci.nsIMsgCompFormat.HTML
: Ci.nsIMsgCompFormat.PlainText;
fields.forcePlainText = composeHtml ? false : true;
params.type = mCompType.New;
return MailServices.compose.OpenComposeWindowWithParams(null, params);
} else {
aBody.match({
plainText: function(body) {
// We're in 2011 now, let's assume everyone knows how to read UTF-8
fields.bodyIsAsciiOnly = false;
fields.characterSet = "UTF-8";
fields.useMultipartAlternative = false;
// So we should have something more elaborate than a simple textarea. The
// reason is, we should be able to differentiate between user-inserted >'s
// and quote-inserted >'s. (The standard Thunderbird plaintext editor does
// it with a blue color). The user-inserted >'s want a space prepended so
// that the MUA doesn't interpret them as quotation. Real quotations don't.
// This is kinda out of scope so we're leaving the issue non-fixed but this
// is clearly a FIXME.
params.format = Ci.nsIMsgCompFormat.PlainText;
fields.forcePlainText = true;
fields.body = simpleWrap(body, 72)+"\n";
let msgLineBreak = isWindows ? "\r\n" : "\n";
fields.body = fields.body.replace(/\r?\n/g, msgLineBreak);
// This part initializes a nsIMsgCompose instance. This is useless, because
// that component is supposed to talk to the "real" compose window, set the
// encoding, set the composition mode... we're only doing that because we
// can't send the message ourselves because of too many [noscript]s.
gMsgCompose = initCompose(MailServices.compose, params);
},
editor: function (iframe) {
fields.bodyIsAsciiOnly = false;
fields.characterSet = "UTF-8";
gMsgCompose = initCompose(
MailServices.compose,
params,
iframe.contentWindow,
iframe.contentWindow.docshell
);
// Here we trust the parameters that have been set by the call to
// msgComposeService.InitCompose above, and we just assume the
// fakeEditor will be able to output HTML and plainText as needed...
let fakeEditor = new FakeEditor(iframe);
gMsgCompose.initEditor(fakeEditor, iframe.contentWindow);
let convertibility = gMsgCompose.bodyConvertible();
Log.debug("This message might be convertible:", convertibility);
switch (convertibility) {
case Ci.nsIMsgCompConvertible.Plain: // 1
case Ci.nsIMsgCompConvertible.Yes: // 2
case Ci.nsIMsgCompConvertible.Altering: // 3
fields.ConvertBodyToPlainText();
fields.forcePlainText = true;
fields.useMultipartAlternative = false;
break;
case Ci.nsIMsgCompConvertible.No: // 4
default:
fields.useMultipartAlternative = true;
break;
}
},
});
// We create a progress listener...
var progress = Cc["@mozilla.org/messenger/progress;1"]
.createInstance(Ci.nsIMsgProgress);
if (progress && progressListener)
progress.registerListener(progressListener);
if (stateListener)
gMsgCompose.RegisterStateListener(stateListener);
try {
gMsgCompose.SendMsg(deliverType, identity, "", null, progress);
return gMsgCompose;
} catch (e) {
Log.error(e);
dumpCallStack(e);
}
}
}