This repository has been archived by the owner on Jun 23, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
/
send.js
493 lines (460 loc) · 18.5 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
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/**
* @fileoverview This file provides a Javascript abstraction for sending a
* message.
* @author Jonathan Protzenko
*/
var EXPORTED_SYMBOLS = ["sendMessage"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const logJSURL = new URL("../log.js", this.__URI__);
XPCOMUtils.defineLazyModuleGetters(this, {
logRoot: logJSURL,
MailServices: "resource:///modules/MailServices.jsm",
MailUtils: "resource:///modules/MailUtils.jsm",
setupLogging: logJSURL,
});
const mCompType = Ci.nsIMsgCompType;
const isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
function importRelative(that, path) {
return ChromeUtils.import(new URL(path, that.__URI__));
}
const { msgUriToMsgHdr } = importRelative(this, "msgHdrUtils.js");
const {
determineComposeHtml,
getEditorForIframe,
plainTextToHtml,
htmlToPlainText,
simpleWrap,
} = importRelative(this, "compose.js");
XPCOMUtils.defineLazyGetter(this, "Log", () => {
return 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 =
msgYear + "-" + (msgDate.getMonth() + 1).toString().padStart(2, "0");
let granularity = identity.archiveGranularity;
let folderUri = identity.archiveFolder;
let archiveFolder = MailUtils.getFolderForURI(folderUri, false);
let forceSingle = !archiveFolder.canCreateSubfolders;
if (
!forceSingle &&
archiveFolder.server instanceof Ci.nsIImapIncomingServer
) {
forceSingle = archiveFolder.server.isGMailServer;
}
if (forceSingle) {
return folderUri;
}
if (granularity >= Ci.nsIMsgIdentity.perYearArchiveFolders) {
folderUri += "/" + msgYear;
}
if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders) {
folderUri += "/" + monthFolderName;
}
return folderUri;
}
/**
* 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;
this.editor = getEditorForIframe(aIframe);
this.editor.QueryInterface(Ci.nsIEditorMailSupport);
}
FakeEditor.prototype = {
getEmbeddedObjects: function _FakeEditor_getEmbeddedObjects() {
return this.editor.getEmbeddedObjects();
},
outputToString: function _FakeEditor_outputToString(formatType, flags) {
return this.editor.outputToString(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: ChromeUtils.generateQI([
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);
}
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.
*/
// eslint-disable-next-line complexity
function sendMessage(
aParams,
{ deliverType, compType },
aBody,
{ progressListener, sendListener, stateListener },
options
) {
let popOut = options && options.popOut;
let archive = options && options.archive;
let { urls, identity, to, subject } = aParams;
let attachments = aParams.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.makeMimeAddress(
identity.fullName,
identity.email
);
fields.to = to;
if ("cc" in aParams) {
fields.cc = aParams.cc;
}
if ("bcc" in aParams) {
fields.bcc = aParams.bcc;
}
fields.subject = subject;
fields.returnReceipt =
"returnReceipt" in aParams
? aParams.returnReceipt
: identity.requestReturnReceipt;
fields.receiptHeaderType =
"receiptType" in aParams ? aParams.receiptType : identity.receiptHeaderType;
fields.DSN =
"requestDsn" in aParams ? aParams.requestDsn : identity.requestDSN;
if ("securityInfo" in aParams) {
fields.securityInfo = aParams.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]);
for (let i = 0; i < msgHdr.numReferences; i++) {
references.push(msgHdr.getStringReference(i));
}
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]);
for (let i = 0; i < msgHdr.numReferences; i++) {
references.push(msgHdr.getStringReference(i));
}
references.push(msgHdr.messageId);
break;
}
case mCompType.ForwardAsAttachment: {
for (let url of 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 = references.map(x => "<" + x + ">");
fields.references = references.join(" ");
for (let x of attachments) {
fields.addAttachment(x);
}
let fccFolder = identity.fccFolder;
let fccSameFolder = identity.fccReplyFollowsParent;
let doFcc = identity.doFcc;
let isReply =
compType == Ci.nsIMsgCompType.Reply ||
compType == Ci.nsIMsgCompType.ReplyAll ||
compType == Ci.nsIMsgCompType.ReplyToGroup ||
compType == Ci.nsIMsgCompType.ReplyToSender ||
compType == Ci.nsIMsgCompType.ReplyToSenderAndGroup ||
compType == Ci.nsIMsgCompType.ReplyWithTemplate;
let defaultFcc = fccFolder.indexOf("nocopy://") == 0 ? "" : fccFolder;
// Replicating the whole logic from nsMsgSend.cpp:2840... with our own archive
// feat.
if (archive) {
// 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.
let folderUri = getArchiveFolderUriFor(identity, new Date());
if (MailUtils.getFolderForURI(folderUri, true)) {
// 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
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!"
);
fields.fcc = defaultFcc;
}
} else if (!doFcc) {
// The user has unchecked "place a copy of the sent message..."
fields.fcc = "";
} else if (isReply && fccSameFolder) {
// "Place a copy of sent messages in the folder of the message being replied
// to..."
let msgHdr = msgUriToMsgHdr(urls[0]);
if (msgHdr.folder.canFileMessages) {
fields.fcc = msgHdr.folder.URI;
} else {
fields.fcc = defaultFcc;
}
} else {
fields.fcc = defaultFcc;
}
// 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(body) {
if (composeHtml) {
fields.body = plainTextToHtml(body);
} else {
fields.body = body;
}
},
editor(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;
params.type = mCompType.New;
MailServices.compose.OpenComposeWindowWithParams(null, params);
return;
}
aBody.match({
plainText(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;
// Strip trailing whitespace before wrapping, so as not to interpret it
// as a f=f line continuation.
fields.body = simpleWrap(body.replace(/ +$/gm, ""), 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(iframe) {
fields.bodyIsAsciiOnly = false;
fields.characterSet = "UTF-8";
gMsgCompose = initCompose(
MailServices.compose,
params,
null, // XXX put a real window here to see the notification window
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...
try {
let fakeEditor = new FakeEditor(iframe);
gMsgCompose.initEditor(fakeEditor, iframe.contentWindow);
} catch (e) {
// Recent Thunderbirds only.
Log.debug(e);
gMsgCompose.editor = getEditorForIframe(iframe);
}
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);
} catch (ex) {
console.error(ex);
}
}