diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/compose | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/compose')
133 files changed, 31727 insertions, 0 deletions
diff --git a/comm/mailnews/compose/.eslintrc.js b/comm/mailnews/compose/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/compose/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/compose/content/sendProgress.js b/comm/mailnews/compose/content/sendProgress.js new file mode 100644 index 0000000000..fbc05451a7 --- /dev/null +++ b/comm/mailnews/compose/content/sendProgress.js @@ -0,0 +1,174 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// dialog is just an array we'll use to store various properties from the dialog document... +var dialog; + +// the msgProgress is a nsIMsgProgress object +var msgProgress = null; + +// random global variables... +var itsASaveOperation = false; +var gBundle; + +window.addEventListener("DOMContentLoaded", onLoad); +window.addEventListener("unload", onUnload); +document.addEventListener("dialogcancel", onCancel); + +// all progress notifications are done through the nsIWebProgressListener implementation... +var progressListener = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + // Set progress meter to show indeterminate. + dialog.progress.removeAttribute("value"); + dialog.progressText.value = ""; + } + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + if (Components.isSuccessCode(aStatus)) { + // we are done sending/saving the message... + // Indicate completion in status area. + let msg; + if (itsASaveOperation) { + msg = gBundle.GetStringFromName("messageSaved"); + } else { + msg = gBundle.GetStringFromName("messageSent"); + } + dialog.status.setAttribute("value", msg); + + // Put progress meter at 100%. + dialog.progress.setAttribute("value", 100); + dialog.progressText.setAttribute( + "value", + gBundle.formatStringFromName("percentMsg", [100]) + ); + } + + // Note: Without some delay closing the window the "msg" string above may + // never be visible. Example: setTimeout(() => window.close(), 1000); + // Windows requires other delays. The delays also cause test failures. + window.close(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + // Calculate percentage. + var percent; + if (aMaxTotalProgress > 0) { + percent = Math.round((aCurTotalProgress / aMaxTotalProgress) * 100); + if (percent > 100) { + percent = 100; + } + + // Advance progress meter. + dialog.progress.value = percent; + + // Update percentage label on progress meter. + dialog.progressText.value = gBundle.formatStringFromName("percentMsg", [ + percent, + ]); + } else { + // Have progress meter show indeterminate with denominator <= 0. + dialog.progress.removeAttribute("value"); + dialog.progressText.value = ""; + } + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + // we can ignore this notification + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + if (aMessage != "") { + dialog.status.setAttribute("value", aMessage); + } + }, + + onSecurityChange(aWebProgress, aRequest, state) { + // we can ignore this notification + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) { + // we can ignore this notification + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +function onLoad() { + // Set global variables. + gBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/sendProgress.properties" + ); + + msgProgress = window.arguments[0]; + if (!msgProgress) { + console.error("Invalid argument to sendProgress.xhtml."); + window.close(); + return; + } + + let subject = ""; + if (window.arguments[1]) { + let progressParams = window.arguments[1].QueryInterface( + Ci.nsIMsgComposeProgressParams + ); + if (progressParams) { + itsASaveOperation = + progressParams.deliveryMode != Ci.nsIMsgCompDeliverMode.Now; + subject = progressParams.subject; + } + } + + if (subject) { + let title = itsASaveOperation + ? "titleSaveMsgSubject" + : "titleSendMsgSubject"; + document.title = gBundle.formatStringFromName(title, [subject]); + } else { + let title = itsASaveOperation ? "titleSaveMsg" : "titleSendMsg"; + document.title = gBundle.GetStringFromName(title); + } + + dialog = {}; + dialog.status = document.getElementById("dialog.status"); + dialog.progress = document.getElementById("dialog.progress"); + dialog.progressText = document.getElementById("dialog.progressText"); + + // set our web progress listener on the helper app launcher + msgProgress.registerListener(progressListener); +} + +function onUnload() { + if (msgProgress) { + try { + msgProgress.unregisterListener(progressListener); + msgProgress = null; + } catch (e) {} + } +} + +// If the user presses cancel, tell the app launcher and close the dialog... +function onCancel(event) { + // Cancel app launcher. + try { + msgProgress.processCanceledByUser = true; + } catch (e) { + return; + } + + // Don't close up dialog, the backend will close the dialog when everything will be aborted. + event.preventDefault(); +} diff --git a/comm/mailnews/compose/content/sendProgress.xhtml b/comm/mailnews/compose/content/sendProgress.xhtml new file mode 100644 index 0000000000..cf46d0c8cc --- /dev/null +++ b/comm/mailnews/compose/content/sendProgress.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<!-- 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 http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/messengercompose/sendProgress.dtd"> +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + style="min-width: 56ch; min-height: 8em" + lightweightthemes="true" + scrolling="false" +> + <head> + <title>&sendDialog.title;</title> + <link rel="localization" href="branding/brand.ftl" /> + <script + defer="defer" + src="chrome://messenger/content/dialogShadowDom.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/messengercompose/sendProgress.js" + ></script> + </head> + <html:body + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + > + <dialog id="sendProgress" buttons="cancel"> + <hbox flex="1"> + <vbox align="end"> + <hbox flex="1" align="center"> + <label value="&status.label;" /> + </hbox> + <hbox flex="1" align="center"> + <label value="&progress.label;" /> + </hbox> + </vbox> + <vbox flex="1"> + <hbox flex="1" align="center"> + <label id="dialog.status" crop="center" /> + </hbox> + <hbox + class="thin-separator" + flex="1" + style="display: flex; align-items: center" + > + <html:progress + id="dialog.progress" + value="0" + max="100" + style="flex: 1" + /> + <label id="dialog.progressText" value="" /> + </hbox> + </vbox> + </hbox> + </dialog> + </html:body> +</html> diff --git a/comm/mailnews/compose/moz.build b/comm/mailnews/compose/moz.build new file mode 100644 index 0000000000..a49689ab64 --- /dev/null +++ b/comm/mailnews/compose/moz.build @@ -0,0 +1,11 @@ +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +DIRS += [ + "public", + "src", +] + +TEST_DIRS += ["test"] diff --git a/comm/mailnews/compose/public/moz.build b/comm/mailnews/compose/public/moz.build new file mode 100644 index 0000000000..b4e027ef05 --- /dev/null +++ b/comm/mailnews/compose/public/moz.build @@ -0,0 +1,30 @@ +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +XPIDL_SOURCES += [ + "nsIMsgAttachment.idl", + "nsIMsgCompFields.idl", + "nsIMsgCompose.idl", + "nsIMsgComposeParams.idl", + "nsIMsgComposeProgressParams.idl", + "nsIMsgComposeSecure.idl", + "nsIMsgComposeService.idl", + "nsIMsgCompUtils.idl", + "nsIMsgCopy.idl", + "nsIMsgQuote.idl", + "nsIMsgQuotingOutputStreamListener.idl", + "nsIMsgSend.idl", + "nsIMsgSendLater.idl", + "nsIMsgSendLaterListener.idl", + "nsIMsgSendListener.idl", + "nsIMsgSendReport.idl", + "nsISmtpServer.idl", + "nsISmtpService.idl", + "nsISmtpUrl.idl", +] + +XPIDL_MODULE = "msgcompose" + +EXPORTS += [] diff --git a/comm/mailnews/compose/public/nsIMsgAttachment.idl b/comm/mailnews/compose/public/nsIMsgAttachment.idl new file mode 100644 index 0000000000..71356d6686 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgAttachment.idl @@ -0,0 +1,144 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(d17d2d60-ec3a-46de-8bd1-24c77dd9b87b)] +interface nsIMsgAttachment : nsISupports { + + /** + * name attribute + * + * @Attachment real name, will be sent with the attachment's header. + * @If no name has been provided, a name will be generated using the url. + */ + attribute AString name; + + /** + * url attribute + * + * @specify where the attachment live (locally or remotely) + */ + attribute AUTF8String url; + + /** + * msgUri attribute + * + * @specify the uri of the message this attachment belongs to + */ + attribute AUTF8String msgUri; + + /** + * urlCharset attribute + * + * @specify the Charset of url (used to convert url to Unicode after + * unescaping) + */ + attribute ACString urlCharset; + + + /** + * temporary attribute + * + * @If set to true, the file pointed by the url will be destroyed when this object is destroyed. + * @This is only for local attachment. + */ + attribute boolean temporary; + + /** + * Are we storing this attachment via a cloud provider and linking to it? + */ + attribute boolean sendViaCloud; + + /** + * Cloud provider account key for this attachment, if any. + */ + attribute ACString cloudFileAccountKey; + + /** + * A data string stored in the x-mozilla-cloud-part header of draft messages, + * to be able to restore cloudFile information of re-opened drafts. + */ + attribute AUTF8String cloudPartHeaderData; + + /** + * This allows the compose front end code to put whatever html annotation + * it wants for the cloud part, e.g., with expiration time, etc. + */ + attribute AUTF8String htmlAnnotation; + + /** + * contentLocation attribute + * + * @Specify the origin url of the attachment, used normally when attaching + * a locally saved html document, but also used for cloud files and to store + * the original mailbox:// url of attachments, after they have been saves as + * temporary files. + */ + attribute ACString contentLocation; + + /** + * contentType attribute + * + * @Specify the content-type of the attachment, this does not include extra content-type parameters. If + * @you need to specify extra information, use contentTypeParam, charset, macType or macCreator. + * @If omitted, it will be determined base on either the name, the url or the content of the file. + */ + attribute string contentType; + + /** + * contentTypeParam attribute + * + * @Specify the any content-type parameter (other than the content-type itself, charset, macType or macCreator). + * @It will be added to the content-type during the send/save operation. + */ + attribute string contentTypeParam; + + /** + * Content-ID for embedded attachments inside a multipart/related container. + */ + attribute AUTF8String contentId; + + /** + * charset attribute + * + * @Specify the charset of the attachment. It will be added to the content-type during the + * @send/save operation + * @If omitted, will be determined automatically (if possible). + */ + attribute string charset; + + /** + * size attribute + * + * @Specify the size of the attachment. + */ + attribute int64_t size; + + /** + * macType attribute + * + * @Specify the Mac file type of the attachment. It will be added to the content-type during the + * @send/save operation + * @If omitted, will be determined automatically on Macintosh OS. + */ + attribute string macType; + + /** + * macCreator attribute + * + * @Specify the Mac file creator of the attachment. It will be added to the content-type during the + * @send/save operation + * @If omitted, will be determined automatically on Macintosh OS. + */ + attribute string macCreator; + + /** + * equalsUrl + * + * @ determines if both attachments have the same url. + */ + boolean equalsUrl(in nsIMsgAttachment attachment); +}; diff --git a/comm/mailnews/compose/public/nsIMsgCompFields.idl b/comm/mailnews/compose/public/nsIMsgCompFields.idl new file mode 100644 index 0000000000..7ed51f4737 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgCompFields.idl @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "msgIStructuredHeaders.idl" + +interface nsIMsgAttachment; +interface nsIMsgComposeSecure; + +/** + * A collection of headers and other attributes for building a mail message. + */ +[scriptable, uuid(10928477-4F24-4357-9397-FBD847F46F0A)] +interface nsIMsgCompFields : msgIWritableStructuredHeaders { + + attribute AString from; + attribute AString replyTo; + attribute AString to; + attribute AString cc; + attribute AString bcc; + readonly attribute bool hasRecipients; + + attribute AString fcc; + attribute AString fcc2; + + attribute AString newsgroups; + attribute string newspostUrl; + attribute AString followupTo; + + attribute AString subject; + + attribute AString organization; + attribute string references; + attribute string priority; + attribute string messageId; + + attribute AString templateName; + // The so-called draft/template ID is a URI in reality. + attribute AUTF8String draftId; + attribute AUTF8String templateId; + + attribute boolean returnReceipt; + attribute long receiptHeaderType; + attribute boolean DSN; + attribute boolean attachVCard; + attribute boolean forcePlainText; + attribute boolean useMultipartAlternative; + attribute boolean bodyIsAsciiOnly; + attribute boolean forceMsgEncoding; + /// Status of manually-activated attachment reminder. + attribute boolean attachmentReminder; + /// Delivery format for the mail being composed + /// (auto = 4, text = 1, html = 2, text and html = 3). + attribute long deliveryFormat; + attribute string contentLanguage; + /// This is populated with the key of the identity which created the draft or template. + attribute string creatorIdentityKey; + + /** + * Beware that when setting this property, your body must be properly wrapped, + * and the line endings must match MSG_LINEBREAK, namely "\r\n" on Windows + * and "\n" on Linux and OSX. + */ + attribute AString body; + + readonly attribute Array<nsIMsgAttachment> attachments; + void addAttachment(in nsIMsgAttachment attachment); + void removeAttachment(in nsIMsgAttachment attachment); + void removeAttachments(); + + /** + * Values for other headers. Headers in order, from the + * mail.compose.other.header pref. + */ + attribute Array<AString> otherHeaders; + + /** + * This function will split the recipients into an array. + * + * @param aRecipients The recipients list to split. + * @param aEmailAddressOnly Set to true to drop display names from the results + * array. + * + * @return An array of the recipients. + */ + Array<AString> splitRecipients(in AString aRecipients, + in boolean aEmailAddressOnly); + + void ConvertBodyToPlainText(); + + /** + * Indicates whether we need to check if the current |DocumentCharset| + * can represent all the characters in the message body. It should be + * initialized to true and set to false when 'Send Anyway' is selected + * by a user. (bug 249530) + */ + attribute boolean needToCheckCharset; + + /** + * Object implementing encryption/signing functionality (e.g. S/MIME, PGP/MIME) + */ + attribute nsIMsgComposeSecure composeSecure; +}; diff --git a/comm/mailnews/compose/public/nsIMsgCompUtils.idl b/comm/mailnews/compose/public/nsIMsgCompUtils.idl new file mode 100644 index 0000000000..18749e1f9d --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgCompUtils.idl @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIMsgIdentity.idl" + +[scriptable, uuid(00b4569a-077e-4236-b993-980fd82bb948)] +interface nsIMsgCompUtils : nsISupports { + string mimeMakeSeparator(in string prefix); + + /** + * Try to use the provided identity and/or host name to generate a message ID. + * + * To identify the host name to use in the message ID, this will: + * - if the attribute "FQDN" of the identity is set to a valid host name, use it + * - otherwise, if the provided host name is valid, use it + * - otherwise, if the identity's email address includes a valid host name after + * an '@' symbol, use it + * - otherwise, bail without generating a message ID (returns with an empty value) + * + * @param nsIMsgIdentity The identity to use to generate the message ID. + * @param string The host to use to generate the message ID. Ignored if empty. + * + * @returns A message ID usable in a Message-ID header, or an empty string + * if no message ID could be generated. + */ + AUTF8String msgGenerateMessageId(in nsIMsgIdentity identity, in AUTF8String host); + + readonly attribute boolean msgMimeConformToStandard; + + /** + * Detect the text encoding of an input string. This is a wrapper of + * mozilla::EncodingDetector to be used by JavaScript code. For C++, use + * MsgDetectCharsetFromFile from nsMsgUtils.cpp instead. + * + * @param aContent The string to detect charset. + * + * @returns Detected charset. + */ + ACString detectCharset(in ACString aContent); +}; diff --git a/comm/mailnews/compose/public/nsIMsgCompose.idl b/comm/mailnews/compose/public/nsIMsgCompose.idl new file mode 100644 index 0000000000..7af2405e84 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgCompose.idl @@ -0,0 +1,305 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIMsgCompFields.idl" +#include "nsIMsgComposeParams.idl" +#include "nsIMsgSendListener.idl" + +%{C++ +#include "nsString.h" +%} + +interface nsIMsgSend; +interface nsIMsgIdentity; +interface nsIMsgProgress; +interface nsIDocShell; +interface mozIDOMWindowProxy; +interface nsIEditor; +interface nsIMsgWindow; + +webidl Element; + +typedef long MSG_ComposeSaveType; + +[scriptable, uuid(6953e50a-7531-11d3-85fe-006008948010)] +interface nsIMsgCompSaveType : nsISupports { + const long File = 0; + const long Template = 1; + const long Draft = 2; +}; + +typedef long MSG_DeliverMode; + +[scriptable, uuid(a9f27dd7-8f89-4de3-8fbf-41b789c16ee5)] +interface nsIMsgCompDeliverMode : nsISupports { + const long Now = 0; + const long Later = 1; + const long Save = 2; + const long SaveAs = 3; + const long SaveAsDraft = 4; + const long SaveAsTemplate = 5; + const long SendUnsent = 6; + const long AutoSaveAsDraft = 7; + const long Background = 8; +}; + +[scriptable, uuid(f38ea280-e090-11d3-a449-e3153319347c)] +interface nsIMsgCompSendFormat : nsISupports { + /* Send only plain text if the message is free of any rich formatting or + * inserted elements. Otherwise send both a HTML part and plain text + * alternative part. */ + const long Auto = 0; + /* Only send a plain text part, losing any rich formatting or inserted + * elements. */ + const long PlainText = 1; + /* Only send a HTML part. */ + const long HTML = 2; + /* Send both the HTML part and the plain text alternative part. */ + const long Both = 3; + /* An unset value, to be set with mail.default_send_format on loading the + * message in the compose window. */ + const long Unset = 4; +}; + +[scriptable, uuid(9638af92-1dd1-11b2-bef1-ca5fee0abc62)] +interface nsIMsgCompConvertible : nsISupports /*ToTXT*/ { + const long Plain = 1; // Like 4.x: Only <html>, <p>, <br>, ... + const long Yes = 2; // *Minor* alterations of the look: <ol>, <dd>, ... + const long Altering = 3; /* Look altered: <strong>, <i>, <h1>, ... + Can be expressed in plaintext, but not in + the way it looked in the HTML composer. */ + const long No = 4; /* Will lose data: <font>, ... + Really *requires* visual formatting or + is not supported by our HTML->TXT converter. */ + /* The values here have meaning, they are "levels": + convertible({a; b}) == max(convertible({a}), convertible({b})) + must be true, i.e. the higher value counts. */ +}; + +[scriptable, uuid(6ce49b2a-07dc-4783-b307-9a355423163f)] +interface nsIMsgComposeStateListener : nsISupports +{ + /* ... */ + void NotifyComposeFieldsReady(); + void ComposeProcessDone(in nsresult aResult); + void SaveInFolderDone(in string folderName); + void NotifyComposeBodyReady(); +}; + +[scriptable, uuid(061aae23-7e0a-4818-9a15-1b5db3ceb7f4)] +interface nsIMsgComposeNotificationType : nsISupports +{ + const long ComposeFieldsReady = 0; + const long ComposeProcessDone = 1; + const long SaveInFolderDone = 2; + const long ComposeBodyReady = 3; +}; + +native nsString(nsString); +[ref] native nsStringRef(nsString); + +[scriptable, uuid(c6544b6b-06dd-43ac-89b5-949d7c81bb7b)] +interface nsIMsgCompose : nsIMsgSendListener { + + /** + * Initializes the msg compose object. + * + * @param aParams An nsIMsgComposeParams object containing the initial + * details for the compose. + * @param aWindow The optional window associated with this compose object. + * @param aDocShell The optional docShell of the editor element that is used + * for composing. + */ + void initialize(in nsIMsgComposeParams aParams, + [optional] in mozIDOMWindowProxy aWindow, + [optional] in nsIDocShell aDocShell); + + /* ... */ + void RegisterStateListener(in nsIMsgComposeStateListener stateListener); + + /* ... */ + void UnregisterStateListener(in nsIMsgComposeStateListener stateListener); + + /* ... */ + Promise sendMsg(in MSG_DeliverMode deliverMode, in nsIMsgIdentity identity, in string accountKey, in nsIMsgWindow aMsgWindow, in nsIMsgProgress progress); + + /** + * After all Compose preparations are complete, send the prepared message to + * the server. This exists primarily to allow an override of the sending to + * use a non-SMTP method for send. + * + * @param deliverMode One of the nsIMsgCompDeliverMode values. + * @param identity The message identity. + * @param accountKey The message account key. + */ + Promise sendMsgToServer(in MSG_DeliverMode deliverMode, + in nsIMsgIdentity identity, + in string accountKey); + + /* ... */ + void CloseWindow(); + + /* ... */ + void abort(); + + /* ... */ + void quoteMessage(in AUTF8String msgURI); + + /* + AttachmentPrettyName will return only the leafName if the it's a file URL. + It will also convert the filename to Unicode assuming it's in the file system + charset. In case of URL, |charset| parameter will be used in the conversion. + This UI utility function should probably go into it's own class + */ + AUTF8String AttachmentPrettyName(in AUTF8String url, in string charset); + + /** + * Expand all mailing lists in the relevant compose fields to include the + * members of their output. This method will additionally update the + * popularity field of cards in the addressing header. + */ + void expandMailingLists(); + + /** + * The level of "convertibility" of the message body (whole HTML document) + * to plaintext. + * + * @return a value from nsIMsgCompConvertible. + */ + long bodyConvertible(); + + /** + * The level of "convertibility" of the provided node to plaintext. + * + * @return a value from nsIMsgCompConvertible. + */ + long nodeTreeConvertible(in Element aNode); + + /** + * The identity currently selected for the message compose object. When set + * this may change the signature on a message being composed. Note that + * typically SendMsg will be called with the same identity as is set here, but + * if it is different the SendMsg version will overwrite this identity. + */ + attribute nsIMsgIdentity identity; + + /* Check if the composing mail headers (and identity) can be converted to a mail charset. + */ + boolean checkCharsetConversion(in nsIMsgIdentity identity, out string fallbackCharset); + + /* The message send object. This is created by default to be the SMTP server + * in sendMsgToServer, but if that method is overridden, set the actual + * value used here. + */ + attribute nsIMsgSend messageSend; + + /* + * Clear the messageSend object to break any circular references + */ + void clearMessageSend(); + + /* ... */ + attribute nsIEditor editor; + + /* ... */ + readonly attribute mozIDOMWindowProxy domWindow; + + /* ... */ + readonly attribute nsIMsgCompFields compFields; + + /* ... */ + readonly attribute boolean composeHTML; + + /* ... */ + attribute MSG_ComposeType type; + + /* ... */ + readonly attribute long wrapLength; + + /* by reading this value, you can determine if yes or not the message has been modified + by the user. When you set this value to false, you reset the modification count + of the body to 0 (clean). + */ + attribute boolean bodyModified; + + /** + * Init the editor THIS USED TO BE [noscript] + * Now, this is called after editor is created, + * which is triggered by loading startup url from JS. + * The completion of document loading is detected by observing + * the "obs_documentCreated" command + */ + void initEditor(in nsIEditor editor, in mozIDOMWindowProxy contentWindow); + + /* The following functions are for internal use, essentially for the listener */ + + /* ... */ + [noscript] void setCiteReference(in nsString citeReference); + + /* Set the URI of the folder where the message has been saved */ + attribute AUTF8String savedFolderURI; + + /* Append the signature defined in the identity to the msgBody */ + [noscript] void processSignature(in nsIMsgIdentity identity, + in boolean aQuoted, + inout nsString aMsgBody); + + /* set any reply flags on the original message's folder */ + [noscript] void processReplyFlags(); + [noscript] void rememberQueuedDisposition(); + + /* ... */ + [noscript] + void convertAndLoadComposeWindow(in nsStringRef aPrefix, + in nsStringRef aBuf, + in nsStringRef aSignature, + in boolean aQuoted, + in boolean aHTMLEditor); + + /* Tell the doc state listeners that the doc state has changed + * aNotificationType is from nsIMsgComposeNotificationType + */ + [noscript] void notifyStateListeners(in long aNotificationType, in nsresult aResult); + + /* Retrieve the progress object */ + readonly attribute nsIMsgProgress progress; + + /* ... */ + [noscript] + void buildBodyMessageAndSignature(); + + /* ... */ + [noscript] void buildQuotedMessageAndSignature(); + + /* ... */ + [noscript] void getQuotingToFollow(out boolean quotingToFollow); + + readonly attribute AUTF8String originalMsgURI; + + attribute boolean deleteDraft; + + /** Set to true when remote content can load in the editor. E.g for pasting. */ + attribute boolean allowRemoteContent; + + /* for easier use of nsIMsgSendListener */ + void addMsgSendListener(in nsIMsgSendListener sendListener); + + /* for easier use of nsIMsgSendListener */ + void removeMsgSendListener(in nsIMsgSendListener sendListener); + + /// Access during mail-set-sender observer if needed, see nsIMsgCompDeliverMode. + readonly attribute MSG_DeliverMode deliverMode; + +}; + +/* send listener interface */ +[scriptable, uuid(ad6ee068-b225-47f9-a50e-8e48440282ca)] +interface nsIMsgComposeSendListener : nsISupports { + + void setMsgCompose(in nsIMsgCompose msgCompose); + void setDeliverMode(in MSG_DeliverMode deliverMode); + +}; diff --git a/comm/mailnews/compose/public/nsIMsgComposeParams.idl b/comm/mailnews/compose/public/nsIMsgComposeParams.idl new file mode 100644 index 0000000000..bafe7ef4e0 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgComposeParams.idl @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + + +#include "nsISupports.idl" +#include "nsIMsgIdentity.idl" +#include "nsIMsgCompFields.idl" +#include "nsIMsgSendListener.idl" + +interface nsIMsgDBHdr; +typedef long MSG_ComposeType; + +[scriptable, uuid(c7035852-7531-11d3-9a73-006008948010)] +interface nsIMsgCompType : nsISupports { + const long New = 0; + const long Reply = 1; + const long ReplyAll = 2; + const long ForwardAsAttachment = 3; + const long ForwardInline = 4; + const long NewsPost = 5; + const long ReplyToSender = 6; + const long ReplyToGroup = 7; + const long ReplyToSenderAndGroup = 8; + const long Draft = 9; + const long Template = 10; // New message from template. + const long MailToUrl = 11; + const long ReplyWithTemplate = 12; + const long ReplyToList = 13; + + /** + * Will resend the original message keeping the Subject and the body the + * same, and will set the Reply-To: header to the sender of the original + * message. This gets the redirector "out of the loop" because replies + * to the message will go to the original sender. This is not the same + * as the Resent mechanism described in section 3.6.6 of RFC 2822, and + * so therefore does not use Resent-* headers. + */ + const long Redirect = 14; + + /** + * Used to compose a new message from an existing message. Links + * are sanitized since the message could be from external sources. + */ + const long EditAsNew = 15; + + /** + * Used to edit an existing template. + */ + const long EditTemplate = 16; +}; + + +typedef long MSG_ComposeFormat; + +[scriptable, uuid(a28325e8-7531-11d3-8f1c-006008948010)] +interface nsIMsgCompFormat : nsISupports{ + const long Default = 0; + const long HTML = 1; + const long PlainText = 2; + const long OppositeOfDefault = 3; +}; + + +[scriptable, uuid(930895f2-d610-43f4-9e3c-25e1d1fe4143)] +interface nsIMsgComposeParams : nsISupports { + attribute MSG_ComposeType type; + attribute MSG_ComposeFormat format; + attribute AUTF8String originalMsgURI; + attribute nsIMsgIdentity identity; + + attribute nsIMsgCompFields composeFields; + attribute boolean bodyIsLink; + + attribute nsIMsgSendListener sendListener; + attribute AString smtpPassword; + attribute nsIMsgDBHdr origMsgHdr; + attribute boolean autodetectCharset; + + /** + * HTML-formatted content to quote in the body of the message. + * Set this to get different content than what would normally + * appear in the body, e.g. the original message body in a reply. + */ + attribute AUTF8String htmlToQuote; +}; diff --git a/comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl b/comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl new file mode 100644 index 0000000000..9a77012c5c --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgComposeProgressParams.idl @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsISupports.idl" +#include "nsIMsgCompose.idl" + +[scriptable, uuid(1e0e7c00-3e4c-11d5-9daa-f88d288130fc)] +interface nsIMsgComposeProgressParams: nsISupports { + + /* message subject */ + attribute wstring subject; + + /* delivery mode */ + attribute MSG_DeliverMode deliveryMode; +}; diff --git a/comm/mailnews/compose/public/nsIMsgComposeSecure.idl b/comm/mailnews/compose/public/nsIMsgComposeSecure.idl new file mode 100644 index 0000000000..cbebdb9495 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgComposeSecure.idl @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIMsgSendReport.idl" +#include "nsISupports.idl" + +interface nsIMsgCompFields; +interface nsIMsgIdentity; +interface nsIOutputStream; +interface nsIX509Cert; + +/** + * Callback type for use with asyncFindCertByEmailAddr. + */ +[scriptable, function, uuid(6149d7d3-14bf-4280-8451-60fb48263894)] +interface nsIDoneFindCertForEmailCallback : nsISupports { + /** + * Called after a searching for a certificate is done. + * + * @param emailAddress - The email address that was used as the key + * to find this certificate. + * @param cert - The valid certificate that was found, + * or null, if no valid cert was found. + */ + void findCertDone(in AUTF8String emailAddress, + in nsIX509Cert cert); +}; + +/** + * An instance of this type is related to exactly one email message + * while the user is composing it. + * Besides remembering flags and providing helper code, it is used to + * cache information about valid S/MIME encryption certificates that + * were found and which may be used at send time. + */ +[scriptable, uuid(245f2adc-410e-4bdb-91e2-a7bb42d61787)] +interface nsIMsgComposeSecure : nsISupports +{ + /** + * Set to true if the outgoing message shall be signed. + */ + attribute boolean signMessage; + + /** + * Set to true if the outgoing message shall be encrypted. + */ + attribute boolean requireEncryptMessage; + + /*************************************************************************** + * The following functions are called during message creation by nsMsgSend, + * after the message source is completely prepared. + ***************************************************************************/ + + /** + * Determine if encryption and/or signing is required. + * + * @param aIdentity - The sender's identity + * @param compFields - Attributes of the composed message + * + * @return - Returns true if the creation of the message requires us to go through + * some encryption work, and false otherwise. + */ + boolean requiresCryptoEncapsulation(in nsIMsgIdentity aIdentity, in nsIMsgCompFields aCompFields); + + /** + * Start encryption work. Called before the encrypted data is processed. + * + * @param aStream - Output stream that takes the resulting data + * @param aRecipients - RFC 2047-encoded list of all recipients (To:, Cc:, Bcc:, ... fields), separated by "," or ", " + * Recipients contain name and email addresses, just like they will be put into the message headers + * @param compFields - Attributes of the composed message + * @param aIdentity - The sender's identity + * @param sendReport - Report feedback to the user + * @param aIsDraft - True if send operation saves draft/template/etc., false if message is really sent (or sent later) + */ + void beginCryptoEncapsulation(in nsIOutputStream aStream, in string aRecipients, in nsIMsgCompFields aCompFields, in nsIMsgIdentity aIdentity, in nsIMsgSendReport sendReport, in boolean aIsDraft); + + /** + * Process a part of the message data. Called multiple times, usually for every + * line of the data to be encrypted + * + * @param aBuf - Buffer holding the data to be processed + * @param aLen - Length of the buffer (number of characters) + */ + void mimeCryptoWriteBlock(in string aBuf, in long aLen); + + /** + * End encryption work. Called after the encrypted data is processed. + * + * @param aAbort - True if the send operation was aborted + * @param sendReport - Report feedback to the user + */ + void finishCryptoEncapsulation(in boolean aAbort, in nsIMsgSendReport sendReport); + + /** + * Is information about a valid encryption certificate for the given + * email address already available in the cache? + * + * @param emailAddress - The email address to check. + * + * @return - True if a valid cert is known by the cache. + */ + boolean haveValidCertForEmail(in AUTF8String emailAddress); + + /** + * If a valid encryption certificate for the given email address + * is already known by the cache, then return the NSS database + * key of that certificate. + * + * @param emailAddress - The email address to check. + * + * @return - NSS db key of the valid cert. + */ + ACString getCertDBKeyForEmail(in AUTF8String emailAddress); + + /** + * Remember the given certificate database key in our cache. The + * given certDBey (as used with nsIX509CertDB) must reference a + * valid encryption certificate for the given email address. + * + * @param emailAddress - The email address that is related to + * the given certDBKey. + * @param certDBKey - The certificate database key. + */ + void cacheValidCertForEmail(in AUTF8String emailAddress, + in ACString certDBKey); + + /* + * Asynchronously find an encryption certificate by email address. Calls + * `findCertDone` function on the provided `nsIDoneFindCertForEmailCallback` + * with the results of the operation. + * + * @param emailAddress - The email address to be used as the key + * to find the certificate. + * @param callback - A callback of type nsIDoneFindCertForEmailCallback, + * function findCertDone will be called with + * the result of the operation. + */ + [must_use] + void asyncFindCertByEmailAddr(in AUTF8String emailAddress, + in nsIDoneFindCertForEmailCallback callback); +}; diff --git a/comm/mailnews/compose/public/nsIMsgComposeService.idl b/comm/mailnews/compose/public/nsIMsgComposeService.idl new file mode 100644 index 0000000000..068bb7bd46 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgComposeService.idl @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + + +#include "nsISupports.idl" +#include "nsIMsgCompose.idl" +#include "nsIMsgComposeParams.idl" + +interface nsIURI; +interface nsIDocShell; +interface nsIMsgWindow; +interface nsIMsgIdentity; +interface nsIMsgIncomingServer; +interface nsIMsgDBHdr; + +webidl Selection; + +[scriptable, uuid(041782bf-e523-444b-a268-d90868fd2b50)] +interface nsIMsgComposeService : nsISupports { + + /** + * Open a compose window given a mailto url and other options. + * + * @param msgComposeWindowURL Can be null in most cases. If you have your + * own chrome url you want to use in bringing up a + * compose window, pass it in here. + * @param msgHdr The header of the original message. + * @param originalMsgURI The URI of the original message. + * @param type The message compose type: new/reply/forward/.. + * @param format The message compose format: text/html/.. + * @param identity The identity to send the message from. + * @param from The email address of the sender. + * @param aMsgWindow The message window to use. + * @param suppressReplyQuote An optional boolean flag to ignore or include + selected content in aMsgWindow as quote in the + new compose window. + */ + [can_run_script] + void OpenComposeWindow(in AUTF8String msgComposeWindowURL, + in nsIMsgDBHdr msgHdr, + in AUTF8String originalMsgURI, + in MSG_ComposeType type, + in MSG_ComposeFormat format, + in nsIMsgIdentity identity, + in AUTF8String from, + in nsIMsgWindow aMsgWindow, + [optional] in Selection aSelection, + [optional] in boolean autodetectCharset); + + /** + * Open a compose window given a mailto url and (optionally) an identity. + * + * @param aMsgComposeWindowURL Can be null in most cases. If you have your + * own chrome url you want to use in bringing up a + * compose window, pass it in here. + * @param aURI The mailto url you want to use as the + * foundation for the data inside the compose + * window. + * @param aIdentity An optional identity to send the message from. + */ + void OpenComposeWindowWithURI(in string msgComposeWindowURL, + in nsIURI aURI, + [optional] in nsIMsgIdentity aIdentity); + + /* ... */ + void OpenComposeWindowWithParams(in string msgComposeWindowURL, in nsIMsgComposeParams params); + + /** + * Creates an nsIMsgCompose instance and initializes it. + * + * @param aParams An nsIMsgComposeParams object containing the initial + * details for the compose. + * @param aWindow The optional window associated with this compose object. + * @param aDocShell The optional docShell of the editor element that is used + * for composing. + */ + nsIMsgCompose initCompose(in nsIMsgComposeParams aParams, + [optional] in mozIDOMWindowProxy aWindow, + [optional] in nsIDocShell aDocShell); + + /** + * defaultIdentity + * + * @return the default identity, in case no identity has been setup yet, will return null + */ + readonly attribute nsIMsgIdentity defaultIdentity; + + /* This function is use for debugging purpose only and may go away at anytime without warning */ + void TimeStamp(in string label, in boolean resetTime); + + /* This attribute is use for debugging purposes for determining whether to PR_LOG or not */ + readonly attribute boolean logComposePerformance; + + [noscript] boolean determineComposeHTML(in nsIMsgIdentity aIdentity, in MSG_ComposeFormat aFormat); + + /** + * given a mailto url, parse the attributes and turn them into a nsIMsgComposeParams object + * @return nsIMsgComposeParams which corresponds to the passed in mailto url + */ + nsIMsgComposeParams getParamsForMailto(in nsIURI aURI); + + /** + * @{ + * These constants control how to forward messages in forwardMessage. + * kForwardAsDefault uses value of pref "mail.forward_message_mode". + */ + const unsigned long kForwardAsDefault = 0; + const unsigned long kForwardAsAttachment = 1; + const unsigned long kForwardInline = 2; + /** @} */ + + /** + * Allow filters to automatically forward a message to the given address(es). + * @param forwardTo the address(es) to forward to + * @param msgHdr the header of the message being replied to + * @param msgWindow message window to use + * @param server server to use for determining which account to send from + * @param aForwardType - How to forward the message one of 3 values: + * kForwardAsDefault, kForwardInline, or + * kForwardAsAttachment. + */ + void forwardMessage(in AString forwardTo, in nsIMsgDBHdr msgHdr, + in nsIMsgWindow msgWindow, in nsIMsgIncomingServer server, + in unsigned long aForwardType); + + /** + * Allow filters to automatically reply to a message. The reply message is + * based on the given template. + * @param msgHdr the header of the message being replied to + * @param templateUri uri of the template to base ther reply on + * @param msgWindow message window to use + * @param server server to use for determining which account to send from + */ + void replyWithTemplate(in nsIMsgDBHdr msgHdr, in AUTF8String templateUri, + in nsIMsgWindow msgWindow, in nsIMsgIncomingServer server); + + /** + * The docShell of each editor element used for composing should be registered + * with this service. docShells passed to initCompose get registered + * automatically. The registrations are typically used to get the msgCompose + * window when determining what remote content to allow to be displayed. + * + * @param aDocShell The nsIDocShell of the editor element. + * @param aMsgCompose The compose object associated with the compose window + */ + void registerComposeDocShell(in nsIDocShell aDocShell, + in nsIMsgCompose aMsgCompose); + + /** + * When an editor docShell is being closed, you should + * unregister it from this service. nsIMsgCompose normally calls this + * automatically for items passed to initCompose. + * + * @param aDocShell The nsIDocShell of the editor element. + */ + void unregisterComposeDocShell(in nsIDocShell aDocShell); + + /** + * For a given docShell, returns the nsIMsgCompose object associated with it. + * + * @param aDocShell The nsIDocShell of the editor element. + * + * @return NS_ERROR_FAILURE if we could not find a nsIMsgCompose for + * the passed in docShell. + */ + nsIMsgCompose getMsgComposeForDocShell(in nsIDocShell aDocShell); +}; diff --git a/comm/mailnews/compose/public/nsIMsgCopy.idl b/comm/mailnews/compose/public/nsIMsgCopy.idl new file mode 100644 index 0000000000..338e7234de --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgCopy.idl @@ -0,0 +1,38 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIMsgFolder.idl" +#include "nsIMsgIdentity.idl" +#include "nsIMsgSend.idl" + +/** + * The contract ID for this component is @mozilla.org/messengercompose/msgcopy;1. + */ +[scriptable, uuid(de03b16f-3a41-40d0-a487-ca21abcf2bee)] +interface nsIMsgCopy : nsISupports { + /** + * Start the process of copying a message file to a message folder. The + * destinationfolder depends on pref and deliver mode. + * + * @param aUserIdentity The identity of the sender + * @param aFile The message file + * @param aMode The deliver mode + * @param aMsgSendObj The nsIMsgSend instance that listens to copy events + * @param aSavePref The folder uri on server + * @param aMsgToReplace The message to replace when copying + */ + void startCopyOperation(in nsIMsgIdentity aUserIdentity, + in nsIFile aFile, + in nsMsgDeliverMode aMode, + in nsIMsgSend aMsgSendObj, + in AUTF8String aSavePref, + in nsIMsgDBHdr aMsgToReplace); + + /** + * Destination folder of the copy operation. Used when aborting copy operation. + */ + readonly attribute nsIMsgFolder dstFolder; +}; diff --git a/comm/mailnews/compose/public/nsIMsgQuote.idl b/comm/mailnews/compose/public/nsIMsgQuote.idl new file mode 100644 index 0000000000..ae8fef2ab9 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgQuote.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsISupports.idl" +#include "nsIMsgQuotingOutputStreamListener.idl" +#include "nsIChannel.idl" +#include "nsIMimeStreamConverter.idl" + +interface nsIMsgDBHdr; + +[scriptable, uuid(f79b1d55-f546-4ed5-9f75-9428e35c4eff)] +interface nsIMsgQuote : nsISupports { + + /** + * Quote a particular message specified by its URI. + * + * @param charset optional parameter - if set, force the message to be + * quoted using this particular charset + */ + void quoteMessage(in AUTF8String msgURI, in boolean quoteHeaders, + in nsIMsgQuotingOutputStreamListener streamListener, + in bool autodetectCharset, in boolean headersOnly, + in nsIMsgDBHdr aOrigHdr); + + readonly attribute nsIMimeStreamConverterListener quoteListener; + readonly attribute nsIChannel quoteChannel; + readonly attribute nsIMsgQuotingOutputStreamListener streamListener; +}; + +[scriptable, uuid(1EC75AD9-88DE-11d3-989D-001083010E9B)] +interface nsIMsgQuoteListener : nsIMimeStreamConverterListener +{ + attribute nsIMsgQuote msgQuote; +}; diff --git a/comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl b/comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl new file mode 100644 index 0000000000..ac86361ab5 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgQuotingOutputStreamListener.idl @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIStreamListener.idl" + +interface nsIMimeHeaders; + +[scriptable, uuid(1fe345e6-2428-4a43-a0c6-d2acea0d4da4)] +interface nsIMsgQuotingOutputStreamListener : nsIStreamListener { + + // The headers are used to fill in the reply's compose fields + void setMimeHeaders(in nsIMimeHeaders headers); + +}; diff --git a/comm/mailnews/compose/public/nsIMsgSend.idl b/comm/mailnews/compose/public/nsIMsgSend.idl new file mode 100644 index 0000000000..9d4639925d --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgSend.idl @@ -0,0 +1,374 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/* + * The nsIMsgSend method will create an RFC822 message and send it all in one operation + * as well as providing the ability to save disk files for later use. The mode of delivery + * can also be specified for the "Send Later", "Drafts" and "Templates" operations. (NOTE: + * This method could easily be broken in to a few different calls. Currently, this method + * does several functions depending on the arguments passed in, but this could easily lead + * to confusion. This is something that very well may change as time allows). + */ +#include "nsISupports.idl" +#include "nsrootidl.idl" +#include "nsIMsgIdentity.idl" +#include "nsIMsgCompFields.idl" +#include "nsIMsgSendListener.idl" +#include "nsIMsgSendReport.idl" +#include "domstubs.idl" +#include "nsIPrompt.idl" +#include "MailNewsTypes2.idl" +#include "nsIMsgComposeParams.idl" + +interface nsIMsgProgress; +interface nsIURI; +interface nsIRequest; +interface nsIMsgDBHdr; +interface nsIMsgHdr; +interface nsIFile; +interface nsIOutputStream; +interface nsIMsgComposeSecure; +interface nsIMsgStatusFeedback; +interface nsIEditor; +interface mozIDOMWindowProxy; + +typedef long nsMsgDeliverMode; + +[scriptable, uuid(c658cd1f-dc4a-43c0-911c-c6d3e569ca7e)] +interface nsIMsgAttachmentData : nsISupports +{ + /// The URL to attach. + attribute nsIURI url; + + /** + * The type to which this document should be + * converted. Legal values are NULL, TEXT_PLAIN + * and APPLICATION_POSTSCRIPT (which are macros + * defined in net.h); other values are ignored. + */ + attribute ACString desiredType; + + /** + * The type of the URL if known, otherwise empty. For example, if + * you were attaching a temp file which was known to contain HTML data, + * you would pass in TEXT_HTML as the realType, to override whatever type + * the name of the tmp file might otherwise indicate. + */ + attribute ACString realType; + + /// Goes along with real_type. + attribute ACString realEncoding; + + /** + * The original name of this document, which will eventually show up in the + * Content-Disposition header. For example, if you had copied a document to a + * tmp file, this would be the original, human-readable name of the document. + */ + attribute ACString realName; + /** + * If you put a string here, it will show up as the Content-Description + * header. This can be any explanatory text; it's not a file name. + */ + attribute ACString description; + + /// mac-specific info + attribute ACString xMacType; + + /// mac-specific info + attribute ACString xMacCreator; +}; + +/** + * When we have downloaded a URL to a tmp file for attaching, this + * represents everything we learned about it (and did to it) in the + * process. + */ +[scriptable, uuid(c552345d-c74b-40b0-a673-79bb461e920b)] +interface nsIMsgAttachedFile : nsISupports +{ + /// Where it came from on the network (or even elsewhere on the local disk.) + attribute nsIURI origUrl; + + /// The tmp file in which the (possibly converted) data now resides. + attribute nsIFile tmpFile; + + /// The type of the data in file_name (not necessarily the same as the type of orig_url.) + attribute ACString type; + + /** + * The encoding of the tmp file. This will be set only if the original + * document had an encoding already; we don't do base64 encoding and so forth + * until it's time to assemble a full MIME message of all parts. + */ + attribute ACString encoding; + /// For Content-Description header. + attribute ACString description; + + /// X-Mozilla-Cloud-Part, if any. + attribute ACString cloudPartInfo; + + attribute ACString xMacType; // mac-specific info + attribute ACString xMacCreator; // mac-specific info + attribute ACString realName; // The real name of the file. + + /** + * Some statistics about the data that was written to the file, so that when + * it comes time to compose a MIME message, we can make an informed decision + * about what Content-Transfer-Encoding would be best for this attachment. + * (If it's encoded already, we ignore this information and ship it as-is.) + */ + attribute unsigned long size; + attribute unsigned long unprintableCount; + attribute unsigned long highbitCount; + attribute unsigned long ctlCount; + attribute unsigned long nullCount; + attribute unsigned long maxLineLength; +}; + +/** + * This interface is used by Outlook import to shuttle embedded + * image information over to nsIMsgSend's createRFC822Message method via + * the aEmbbeddedObjects parameter. + */ +[scriptable, uuid(5d2c6554-b4c8-4d68-b864-50e0df929707)] +interface nsIMsgEmbeddedImageData : nsISupports +{ + attribute nsIURI uri; + attribute ACString cid; + attribute ACString name; +}; + +[ptr] native nsMsgAttachedFile(nsMsgAttachedFile); + +[scriptable, uuid(747fdfa2-1754-4282-ab26-1e55fd8de13c)] +interface nsIMsgSend : nsISupports +{ + // + // This is the primary interface for creating and sending RFC822 messages + // in the new architecture. Currently, this method supports many arguments + // that change the behavior of the operation. This will change in time to + // be separate calls that will be more singluar in nature. + // + // NOTE: when aEditor is non-null, a multipart related MHTML message will + // be created + // + + /// Send the message straight away. + const nsMsgDeliverMode nsMsgDeliverNow = 0; + /** + * Queue the message for sending later, but then wait for the user to + * request to send it. + */ + const nsMsgDeliverMode nsMsgQueueForLater = 1; + const nsMsgDeliverMode nsMsgSave = 2; + const nsMsgDeliverMode nsMsgSaveAs = 3; + const nsMsgDeliverMode nsMsgSaveAsDraft = 4; + const nsMsgDeliverMode nsMsgSaveAsTemplate = 5; + const nsMsgDeliverMode nsMsgSendUnsent = 6; + + /// Queue the message in the unsent folder and send it in the background. + const nsMsgDeliverMode nsMsgDeliverBackground = 8; + + /** + * Create an rfc822 message and send it. + * @param aEditor nsIEditor instance that contains message. May be a dummy, + * especially in the case of import. + * @param aUserIdentity identity to send from. + * @param aAccountKey account we're sending message from. May be null. + * @param aFields composition fields from addressing widget + * @param aIsDigest is this a digest message? + * @param aDontDeliver Set to false by the import code - used when we're + * trying to create a message from parts. + * @param aMode delivery mode + * @param aMsgToReplace e.g., when saving a draft over an old draft. May be 0 + * @param aBodyType content type of message body + * @param aBody message body text (should have native line endings) + * @param aParentWindow compose window; may be null. + * @param aProgress where to send progress info; may be null. + * @param aListener optional listener for send progress + * @param aPassword optional smtp server password + * @param aOriginalMsgURI may be null. + * @param aType see nsIMsgComposeParams.idl + */ + Promise createAndSendMessage(in nsIEditor aEditor, + in nsIMsgIdentity aUserIdentity, + in string aAccountKey, + in nsIMsgCompFields aFields, + in boolean aIsDigest, + in boolean aDontDeliver, + in nsMsgDeliverMode aMode, + in nsIMsgDBHdr aMsgToReplace, + in string aBodyType, + in AString aBody, + in mozIDOMWindowProxy aParentWindow, + in nsIMsgProgress aProgress, + in nsIMsgSendListener aListener, + in AString aPassword, + in AUTF8String aOriginalMsgURI, + in MSG_ComposeType aType); + + /** + * Creates a file containing an rfc822 message, using the passed information. + * aListener's OnStopSending method will get called with the file the message + * was stored in. OnStopSending may be called sync or async, depending on + * content, so you need to handle both cases. + * + * @param aUserIdentity The user identity to use for sending this email. + * @param aFields An nsIMsgCompFields object containing information + * on who to send the message to. + * @param aBodyType content type of message body + * @param aBody message body text (should have native line endings) + * @param aCreateAsDraft If true, this message will be put in a drafts folder + * @param aAttachments Array of nsIMsgAttachedFile objects + * @param aEmbeddedObjects Array of nsIMsgEmbeddedImageData objects for + * MHTML messages. + * @param aListener listener for msg creation progress and resulting file. + */ + void createRFC822Message(in nsIMsgIdentity aUserIdentity, + in nsIMsgCompFields aFields, + in string aBodyType, + in ACString aBody, + in boolean aCreateAsDraft, + in Array<nsIMsgAttachedFile> aAttachments, + in Array<nsIMsgEmbeddedImageData> aEmbeddedObjects, + in nsIMsgSendListener aListener); + + /** + * Sends a file to the specified composition fields, via the user identity + * provided. + * + * @param aUserIdentity The user identity to use for sending this email. + * @param aAccountKey The key of the account that this message relates + * to. + * @param aFields An nsIMsgCompFields object containing information + * on who to send the message to. + * @param aSendIFile A reference to the file to send. + * @param aDeleteSendFileOnCompletion + * Set to true if you want the send file deleted once + * the message has been sent. + * @param aDigest If this is a multipart message, this param + * specifies whether the message is in digest or mixed + * format. + * @param aMode The delivery mode for sending the message (see + * above for values). + * @param aMsgToReplace A message header representing a message to be + * replaced by the one sent, this param may be null. + * @param aListener An nsIMsgSendListener to receive feedback on the + * current send status. This parameter can also + * support the nsIMsgCopyServiceListener interface to + * receive notifications of copy finishing e.g. after + * saving a message to the sent mail folder. + * This param may be null. + * @param aStatusFeedback A feedback listener for slightly different feedback + * on the message send status. This param may be null. + * @param aPassword Pass this in to prevent a dialog if the password + * is needed for secure transmission. + */ + Promise sendMessageFile(in nsIMsgIdentity aUserIdentity, + in string aAccountKey, + in nsIMsgCompFields aFields, + in nsIFile aSendIFile, + in boolean aDeleteSendFileOnCompletion, + in boolean aDigest, + in nsMsgDeliverMode aMode, + in nsIMsgDBHdr aMsgToReplace, + in nsIMsgSendListener aListener, + in nsIMsgStatusFeedback aStatusFeedback, + in wstring aPassword + ); + + /* Abort current send/save operation */ + void abort(); + + /** + * Report a send failure. + * + * @param aFailureCode The failure code of the send operation. See + * nsComposeStrings.h for possible values. NS_OK is a possible + * value as well; if passed, the function won't prompt the user * but will still about the session. + * @param aErrorMsg The appropriate error string for the failure. + * @result A modified result value in the case a user action results in + * a different way to handle the failure. + */ + nsresult fail(in nsresult aFailureCode, in wstring aErrorMsg); + + /* Disable UI notification (alert message) */ + void setGUINotificationState(in boolean aEnableFlag); + + /* Crypto */ + void BeginCryptoEncapsulation(); + + /* retrieve the last send process report*/ + readonly attribute nsIMsgSendReport sendReport; + + /* methods for send listener ... */ + void notifyListenerOnStartSending(in string aMsgID, in unsigned long aMsgSize); + void notifyListenerOnProgress(in string aMsgID, in unsigned long aProgress, in unsigned long aProgressMax); + void notifyListenerOnStatus(in string aMsgID, in wstring aMsg); + void notifyListenerOnStopSending(in string aMsgID, in nsresult aStatus, in wstring aMsg, in nsIFile returnFile); + void notifyListenerOnTransportSecurityError(in string msgID, in nsresult status, in nsITransportSecurityInfo secInfo, in ACString location); + void deliverAsMailExit(in nsIURI aUrl, in nsresult aExitCode); + void deliverAsNewsExit(in nsIURI aUrl, in nsresult aExitCode); + + void sendDeliveryCallback(in nsIURI aUrl, in boolean inIsNewsDelivery, in nsresult aExitCode); + + /* methods for copy listener ... */ + void notifyListenerOnStartCopy(); + void notifyListenerOnProgressCopy(in unsigned long aProgress, in unsigned long aProgressMax); + void notifyListenerOnStopCopy(in nsresult aStatus); + void getMessageId(out ACString messageID); + /// When saving as draft, the folder uri we saved to. + readonly attribute AUTF8String folderUri; + + /** + * After a draft is saved, use this to get the mime part number for the dom + * node in the editor embedded object list with the passed in index. + * + * @param aDomIndex - index in the editor dom embedded object list of + * the part we're interested in. These are generally images. + * + * @return the mime part number for that object. + */ + ACString getPartForDomIndex(in long aDomIndex); + + attribute nsMsgKey messageKey; + + /* process attachment */ + void gatherMimeAttachments(); + readonly attribute boolean processAttachmentsSynchronously; + readonly attribute unsigned long attachmentCount; + attribute unsigned long pendingAttachmentCount; + readonly attribute nsMsgDeliverMode deliveryMode; + + nsIMsgProgress getProgress(); + + nsIOutputStream getOutputStream(); + + attribute nsIRequest runningRequest; + + attribute nsresult status; + + attribute nsIMsgComposeSecure cryptoclosure; + + /// Access the local copy of the composition fields. + readonly attribute nsIMsgCompFields sendCompFields; + + /// The message body. + readonly attribute AString sendBody; + + /// The type of the message body (typically text/plain or text/html). + readonly attribute ACString sendBodyType; + + /// The identity to use to send the message. + readonly attribute nsIMsgIdentity identity; + + /// The folder name to which the message will be saved, + /// used by error reporting. + attribute AString savedToFolderName; + + /// Should we deliver this message (versus saving as a file)? + attribute boolean dontDeliver; + +}; diff --git a/comm/mailnews/compose/public/nsIMsgSendLater.idl b/comm/mailnews/compose/public/nsIMsgSendLater.idl new file mode 100644 index 0000000000..65fecebced --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgSendLater.idl @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIStreamListener.idl" + +interface nsIMsgStatusFeedback; +interface nsIMsgIdentity; +interface nsIMsgSendLaterListener; +interface nsIMsgFolder; + +/** + * nsIMsgSendLater is a service used for sending messages in the background. + * Messages should be saved to an identity's unsent messages folder, and then + * can be sent by calling sendUnsentMessages. + * + * Although the service supports passing identities as parameters, until bug + * 317803 is fixed, all identities use the same folder, and hence the option + * currently doesn't work. + */ +[scriptable, uuid(fa324a4b-4b87-4e9a-a3c0-af9071a358df)] +interface nsIMsgSendLater : nsIStreamListener +{ + /// Used to obtain status feedback for when messages are sent. + attribute nsIMsgStatusFeedback statusFeedback; + + /** + * Sends any unsent messages in the identity's unsent messages folder. + * + * @param aIdentity The identity to send messages for. + */ + void sendUnsentMessages(in nsIMsgIdentity aIdentity); + + /** + * Adds an listener to the service to receive notifications. + * + * @param aListener The listener to add. + */ + void addListener(in nsIMsgSendLaterListener aListener); + + /** + * Removes a listener from the service. + * + * @param aListener The listener to remove. + * @exception NS_ERROR_INVALID_ARG If the listener was not already added to + * the service. + */ + void removeListener(in nsIMsgSendLaterListener aListener); + + /** + * Returns the unsent messages folder for the identity. + */ + nsIMsgFolder getUnsentMessagesFolder(in nsIMsgIdentity userIdentity); + + /** + * Returns true if there are any unsent messages to send. + * + * @param aIdentity The identity whose folder to check for unsent messages. + * If not specified, all unsent message folders are checked. + */ + boolean hasUnsentMessages([optional] in nsIMsgIdentity aIdentity); + + /// Returns true if the service is currently sending messages. + readonly attribute boolean sendingMessages; +}; diff --git a/comm/mailnews/compose/public/nsIMsgSendLaterListener.idl b/comm/mailnews/compose/public/nsIMsgSendLaterListener.idl new file mode 100644 index 0000000000..6e6e29a794 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgSendLaterListener.idl @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgDBHdr; +interface nsIMsgIdentity; + +/** + * Implement this interface and add to nsIMsgSendLater to receive notifications + * of send later actions. + */ +[scriptable, uuid(a7bc603b-d0da-4959-a82f-4b99c138b9f4)] +interface nsIMsgSendLaterListener : nsISupports { + /** + * Notify the observer that the operation of sending messages later has + * started. + * + * @param aTotalMessageCount Number of messages to be sent. This will not + * change over the time we are doing this sequence. + */ + void onStartSending(in unsigned long aTotalMessageCount); + + /** + * Notify the observer that the next message send/copy is starting and + * provide details about the message. + * + * @param aCurrentMessage The current message number that is being sent. + * @param aTotalMessageCount The total number of messages that we are + * trying to send. + * @param aMessageHeader The header information for the message that is + * being sent. + * @param aMessageIdentity The identity being used to send the message. + */ + void onMessageStartSending(in unsigned long aCurrentMessage, + in unsigned long aTotalMessageCount, + in nsIMsgDBHdr aMessageHeader, + in nsIMsgIdentity aIdentity); + + /** + * Notify the observer of the current progress of sending a message. The one + * function covers sending the message over the network and copying to the + * appropriate sent folder. + * + * @param aCurrentMessage The current message number that is being sent. + * @param aTotalMessageCount The total number of messages that we are + * trying to send. + * @param aMessageSendPercent The percentage of the message sent (0 to 100) + * @param aMessageCopyPercent The percentage of the copy completed (0 to + * 100). If there is no copy for this message, + * this may be set to 100 at the same time as + * aMessageSendPercent. + */ + void onMessageSendProgress(in unsigned long aCurrentMessage, + in unsigned long aTotalMessageCount, + in unsigned long aMessageSendPercent, + in unsigned long aMessageCopyPercent); + + /** + * Notify the observer of an error in the send message later function. + * + * @param aCurrentMessage The current message number that is being sent. + * @param aMessageHeader The header information for the message that is + * being sent. + * @param aStatus The error status code. + * @param aMsg A text string describing the error. + */ + void onMessageSendError(in unsigned long aCurrentMessage, + in nsIMsgDBHdr aMessageHeader, + in nsresult aStatus, + in wstring aMsg); + + /** + * Notify the observer that the send unsent messages operation has finished. + * This is called regardless of the success/failure of the operation. + * + * @param aStatus Status code for the message send. + * @param aMsg A text string describing the error. + * @param aTotalTried Total number of messages that were attempted to be sent. + * @param aSuccessful How many messages were successfully sent. + */ + void onStopSending(in nsresult aStatus, in wstring aMsg, + in unsigned long aTotalTried, in unsigned long aSuccessful); +}; diff --git a/comm/mailnews/compose/public/nsIMsgSendListener.idl b/comm/mailnews/compose/public/nsIMsgSendListener.idl new file mode 100644 index 0000000000..0bca8cceb0 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgSendListener.idl @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIFile; +interface nsITransportSecurityInfo; + +[scriptable, uuid(D34DC178-5E78-45E8-8658-A8F52D9CCF5F)] +interface nsIMsgSendListener : nsISupports { + + /** + * Notify the observer that the message has started to be delivered. This method is + * called only once, at the beginning of a message send operation. + * + * @return The return value is currently ignored. In the future it may be + * used to cancel the URL load.. + */ + void onStartSending(in string aMsgID, in uint32_t aMsgSize); + + /** + * Notify the observer that progress as occurred for the message send + */ + void onProgress(in string aMsgID, in uint32_t aProgress, in uint32_t aProgressMax); + + /** + * Notify the observer with a status message for the message send + */ + void onStatus(in string aMsgID, in wstring aMsg); + + /** + * Notify the observer that the message has been sent. This method is + * called once when the networking library has finished processing the + * message. + * + * This method is called regardless of whether the the operation was successful. + * aMsgID The message id for the mail message + * status Status code for the message send. + * msg A text string describing the error. + * returnFileSpec The returned file spec for save to file operations. + */ + void onStopSending(in string aMsgID, in nsresult aStatus, in wstring aMsg, + in nsIFile aReturnFile); + + /** + * Notify the observer with the message id and the folder uri before the draft + * is copied. + */ + void onGetDraftFolderURI(in string aMsgID, in AUTF8String aFolderURI); + + /** + * Notify the observer when the user aborts the send without actually doing the send + * eg : by closing the compose window without Send. + */ + void onSendNotPerformed(in string aMsgID, in nsresult aStatus); + + /** + * Notify that an NSS security error has occurred during the send + * (e.g. Bad Certificate or SSL version failure). + * This callback is invoked before onStopSending(), in case a listener + * needs the securityInfo - most likely to get at a failed certificate, + * allowing the user to add an exception. + * onStopSending() will still be called after this, so a listener + * which doesn't need special NSS handling can just leave this callback as + * an empty function and leave the handling to onStopSending(). + * + * @param {string} msgID - The message ID. + * @param {nsresult} status - The error code (it will be in the NSS error + * code range). + * @param {nsITransportSecurityInfo} secInfo + * - Security info for the failed operation. + * @param {ACString} location - The location of the failed operation + * ("<host>:<port>") + */ + void onTransportSecurityError(in string msgID, in nsresult status, in nsITransportSecurityInfo secInfo, in ACString location); + +}; diff --git a/comm/mailnews/compose/public/nsIMsgSendReport.idl b/comm/mailnews/compose/public/nsIMsgSendReport.idl new file mode 100644 index 0000000000..bcdf8e7265 --- /dev/null +++ b/comm/mailnews/compose/public/nsIMsgSendReport.idl @@ -0,0 +1,47 @@ +/* -*- Mode: idl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; + +[scriptable, uuid(2ec81175-bc65-44b9-ba87-462bc3f938db)] +interface nsIMsgProcessReport : nsISupports { + + attribute boolean proceeded; + attribute nsresult error; + attribute wstring message; + + void reset(); +}; + +[scriptable, uuid(428c5bde-29f5-4bfe-830a-ec795a1c2975)] +interface nsIMsgSendReport : nsISupports { + + const long process_Current = -1; + const long process_BuildMessage = 0; + const long process_NNTP = 1; + const long process_SMTP = 2; + const long process_Copy = 3; + const long process_Filter = 4; + const long process_FCC = 5; + + attribute long deliveryMode; /* see nsMsgDeliverMode in nsIMsgSend.idl for valid value */ + attribute long currentProcess; + + void reset(); + + void setProceeded(in long process, in boolean proceeded); + void setError(in long process, in nsresult error, in boolean overwriteError); + void setMessage(in long process, in wstring message, in boolean overwriteMessage); + + nsIMsgProcessReport getProcessReport(in long process); + + /* Display Report will ananlyze data collected during the send and will show the most appropriate error. + Also it will return the error code. In case of no error or if the error has been canceld, it will return + NS_OK. + */ + nsresult displayReport(in mozIDOMWindowProxy prompt, in boolean showErrorOnly, in boolean dontShowReportTwice); +}; diff --git a/comm/mailnews/compose/public/nsISmtpServer.idl b/comm/mailnews/compose/public/nsISmtpServer.idl new file mode 100644 index 0000000000..6180e9289d --- /dev/null +++ b/comm/mailnews/compose/public/nsISmtpServer.idl @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +interface nsIAuthPrompt; +interface nsIUrlListener; +interface nsIURI; +interface nsIMsgWindow; + +/** + * This interface represents a single SMTP Server. A SMTP server instance may be + * created/obtained from nsIMsgAccountManager. + * + * Most of the attributes will set/get preferences from the main preferences + * file. + */ +[scriptable, uuid(a53dce6c-cd81-495c-83bc-45a65df1f08e)] +interface nsISmtpServer : nsISupports { + + /// A unique identifier for the server. + attribute string key; + + /** + * A unique identifier for this server that can be used for the same + * server synced across multiple profiles. Auto-generated on first use. + */ + attribute AUTF8String UID; + + /// A user supplied description for the server. + attribute AUTF8String description; + + /// The server's hostname. + attribute AUTF8String hostname; + + /// The server's port. + attribute int32_t port; + + /// The username to access the server with (if required) + attribute ACString username; + + /** + * The CLIENTID to use for this server (if required). + * @see https://tools.ietf.org/html/draft-storey-smtp-client-id-05 + */ + attribute ACString clientid; + + /** + * Whether the CLIENTID feature above is enabled. + */ + attribute boolean clientidEnabled; + + /** + * The password to access the server with (if required). + * + * @note this is stored within the server instance but not within preferences. + * It can be specified/saved here to avoid prompting the user constantly for + * the sending password. + */ + attribute AString password; + + /// Returns a displayname of the format hostname:port or just hostname + readonly attribute string displayname; + + /** + * Authentication mechanism. + * + * @see nsMsgAuthMethod (in MailNewsTypes2.idl) + * Same as "mail.smtpserver...authMethod" pref + * + * Compatibility note: This attribute had a different meaning in TB < 3.1 + */ + attribute nsMsgAuthMethodValue authMethod; + + /** + * Whether to SSL or STARTTLS or not + * + * @see nsMsgSocketType (in MailNewsTypes2.idl) + * Same as "mail.smtpserver...try_ssl" pref + */ + attribute nsMsgSocketTypeValue socketType; + + /** + * May contain an alternative argument to EHLO or HELO to provide to the + * server. Reflects the value of the mail.smtpserver.*.hello_argument pref. + * This is mainly useful where ISPs don't bother providing PTR records for + * their servers and therefore users get an error on sending. See bug 244030 + * for more discussion. + */ + readonly attribute ACString helloArgument; + + /// Returns the URI of the server (smtp:///) + readonly attribute AUTF8String serverURI; + + /** Limit of concurrent connections to a server. */ + attribute long maximumConnectionsNumber; + + /** Close cached server connections. */ + void closeCachedConnections(); + + /** + * Gets a password for this server, using a UI prompt if necessary. + * + * @param promptString The string to prompt the user with when asking for + * the password. + * @param promptTitle The title of the prompt. + * @return The password to use (may be null if no password was + * obtained). + */ + AString getPasswordWithUI(in wstring promptString, in wstring promptTitle); + + /** + * Gets a username and password for this server, using a UI prompt if + * necessary. + * + * @param promptString The string to prompt the user with when asking for + * the password. + * @param promptTitle The title of the prompt. + * @param netPrompt An nsIAuthPrompt instance to use for the password + * prompt. + * @param userid The username to use (may be null if no password was + * obtained). + * @param password The password to use (may be empty if no password was + * obtained). + */ + void getUsernamePasswordWithUI(in wstring promptString, in wstring promptTitle, + in nsIAuthPrompt netPrompt, out ACString userid, + out AString password); + + /** + * Calling this will *remove* the saved password for this server from the + * password manager and from the stored value. + */ + void forgetPassword(); + + /** + * Verify that we can logon + * + * @param aPassword - password to use + * @param aUrlListener - gets called back with success or failure. + * @return - the url that we run. + * + */ + nsIURI verifyLogon(in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow); + + /// Call this to clear all preference values for this server. + void clearAllValues(); +}; diff --git a/comm/mailnews/compose/public/nsISmtpService.idl b/comm/mailnews/compose/public/nsISmtpService.idl new file mode 100644 index 0000000000..0fc306fe11 --- /dev/null +++ b/comm/mailnews/compose/public/nsISmtpService.idl @@ -0,0 +1,134 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsISmtpServer; +interface nsIURI; +interface nsIUrlListener; +interface nsIMsgIdentity; +interface nsIInterfaceRequestor; +interface nsIFile; +interface nsIMsgStatusFeedback; +interface nsIRequest; +interface nsIMsgWindow; + +[scriptable, uuid(1b11b532-1527-4fc0-a00f-4ce7e6886419)] +interface nsISmtpService : nsISupports { + /** + * Sends a mail message via the given parameters. This function builds an + * SMTP URL and makes an SMTP connection, and then runs the url. + * The SMTP server defined + * in the aSenderIdentity object (see nsIMsgIdentity) will be used to send + * the message. If there is no SMTP server defined in aSenderIdentity, the + * default SMTP server will be used. + * + * @note The file to send must be in the format specified by RFC 2822 for + * sending data. This includes having the correct CRLF line endings + * throughout the file, and the <CRLF>.<CRLF> at the end of the file. + * sendMailMessage does no processing/additions on the file. + * + * @param aFilePath The file to send. + * @param aRecipients A comma delimited list of recipients. + * @param aSenderIdentity The identity of the sender. + * @param aSender The senders email address. + * @param aPassword Pass this in to prevent a dialog if the + * password is needed for secure transmission. + * @param aUrlListener A listener to listen to the URL being run, + * this parameter may be null. + * @param aStatusListener A feedback listener for slightly different + * feedback on the message send status. This + * parameter may be null. + * @param aNotificationCallbacks More notification callbacks + * @param aRequestDSN Pass true to request Delivery Status + * Notification. + * @param aMessageId The message id can be used as ENVID for DSN. + * @param aURL Provides a handle on the running url. You + * can later interrupt the action by asking the + * netlib service manager to interrupt the url + * you are given back. This parameter may be + * null. + * @param aRequest Provides a handle to the running request. + * This parameter may be null. + */ + void sendMailMessage(in nsIFile aFilePath, in string aRecipients, + in nsIMsgIdentity aSenderIdentity, + in string aSender, + in AString aPassword, + in nsIUrlListener aUrlListener, + in nsIMsgStatusFeedback aStatusListener, + in nsIInterfaceRequestor aNotificationCallbacks, + in boolean aRequestDSN, + in ACString aMessageId, + out nsIURI aURL, + out nsIRequest aRequest); + + /** + * Verifies that we can logon to the server with given password + * + * @param aSmtpServer Server to try to logon to. + * @param aUrlListener Listener that will get notified whether logon + * was successful or not. + * @param aMsgWindow nsIMsgWindow to use for notification callbacks. + * @return - the url that we run. + */ + nsIURI verifyLogon(in nsISmtpServer aServer, in nsIUrlListener aListener, + in nsIMsgWindow aMsgWindow); + + /** + * Return the SMTP server that is associated with an identity. + * @param aSenderIdentity the identity + */ + nsISmtpServer getServerByIdentity(in nsIMsgIdentity aSenderIdentity); + + /** + * A copy of the array of SMTP servers, as stored in the preferences + */ + readonly attribute Array<nsISmtpServer> servers; + + /** + * The default server, across sessions of the app + * (eventually there will be a session default which does not + * persist past shutdown) + */ + attribute nsISmtpServer defaultServer; + + /** + * The "session default" server - this is never saved, and only used + * for the current session. Always falls back to the default server + * unless explicitly set. + */ + attribute nsISmtpServer sessionDefaultServer; + + /** + * Create a new SMTP server. + * Use this instead of createInstance(), so that the SMTP Service can + * be aware of this server + */ + nsISmtpServer createServer(); + + /** + * Find the first server with the given hostname and/or username. + * Note: if either username or hostname is empty, then that parameter will + * not be used in the matching process. + * @param username the username for the server + * @param hostname the hostname of the server + * @returns null if no server is found + */ + nsISmtpServer findServer(in string username, in string hostname); + + /** + * Look up the server with the given key. + */ + nsISmtpServer getServerByKey(in string key); + + /** + * Delete the given server from the server list - does nothing if the server + * does not exist + * @param server the server to delete. Use findServer() if you only know + * the hostname + */ + void deleteServer(in nsISmtpServer server); +}; diff --git a/comm/mailnews/compose/public/nsISmtpUrl.idl b/comm/mailnews/compose/public/nsISmtpUrl.idl new file mode 100644 index 0000000000..c03bc7f4e6 --- /dev/null +++ b/comm/mailnews/compose/public/nsISmtpUrl.idl @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIMsgComposeParams.idl" + +interface nsIMsgIdentity; +interface nsIPrompt; +interface nsIAuthPrompt; +interface nsISmtpServer; +interface nsIInterfaceRequestor; +interface nsIFile; + +[scriptable, uuid(da22b8ac-059d-4f82-bf99-f5f3d3c8202d)] +interface nsISmtpUrl : nsISupports { + /** + * SMTP Parse specific getters. + * These retrieve various parts from the url. + */ + + /** + * This list is a list of all recipients to send the email to. + * Each name is NULL terminated. + */ + attribute string recipients; + + attribute boolean PostMessage; + + /** + * The message can be stored in a file, to allow accessors for getting and + * setting the file name to post. + */ + attribute nsIFile postMessageFile; + + attribute boolean requestDSN; + + /** + * The envid which is used in the DSN. + */ + attribute ACString dsnEnvid; + + /** + * The sender of this mail (can be different from identity). + */ + attribute string sender; + + /** + * SMTP Url instance specific getters and setters + * Information the protocol needs to know in order to run the url. + * These are NOT event sinks which are things the caller needs to know. + */ + + /** + * By default the url is really a bring up the compose window mailto url. + * You need to call this function if you want to force the message to be + * posted to the mailserver. + */ + + /** + * The user's full name and user's email address are encapsulated in the + * senderIdentity. + * (the user's domain name can be glopped from the user's email address) + * + * NOTE: the SMTP username and SMTP server are in the smtp url + * smtp://sspitzer@tintin/... + */ + attribute nsIMsgIdentity senderIdentity; + attribute nsIPrompt prompt; + attribute nsIAuthPrompt authPrompt; + attribute nsIInterfaceRequestor notificationCallbacks; + attribute nsISmtpServer smtpServer; + + attribute boolean verifyLogon; // we're just verifying the ability to logon + + /// Constant for the default SMTP port number + const int32_t DEFAULT_SMTP_PORT = 25; + + /// Constant for the default SMTP over ssl port number + const int32_t DEFAULT_SMTPS_PORT = 465; +}; + +[scriptable, uuid(87c36c23-4bc2-4992-b338-69f88f6ed0a1)] +interface nsIMailtoUrl : nsISupports { + /** + * mailto: parse specific getters + * + * All of these fields are things we can effectively extract from a + * mailto url if it contains all of these values + * + * Note: Attachments aren't available because that would expose a potential + * security hole (see bug 99055). + * + * These items are in one function as we only ever get them from the one + * place and all at the same time. + */ + void getMessageContents(out AUTF8String aToPart, out AUTF8String aCcPart, + out AUTF8String aBccPart, out AUTF8String aSubjectPart, + out AUTF8String aBodyPart, out AUTF8String aHtmlPart, + out ACString aReferencePart, + out AUTF8String aNewsgroupPart, + out MSG_ComposeFormat aFormat); + + /** + * These attributes are available should mailnews or extensions want them + * but aren't used by standard in mailnews. + */ + readonly attribute AUTF8String fromPart; + readonly attribute AUTF8String followUpToPart; + readonly attribute AUTF8String organizationPart; + readonly attribute AUTF8String replyToPart; + readonly attribute AUTF8String priorityPart; + readonly attribute AUTF8String newsHostPart; +}; diff --git a/comm/mailnews/compose/src/MailtoProtocolHandler.jsm b/comm/mailnews/compose/src/MailtoProtocolHandler.jsm new file mode 100644 index 0000000000..a129e7d750 --- /dev/null +++ b/comm/mailnews/compose/src/MailtoProtocolHandler.jsm @@ -0,0 +1,38 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MailtoProtocolHandler"]; + +/** + * Protocol handler for mailto: url. + * + * @implements {nsIProtocolHandler} + */ +class MailtoProtocolHandler { + QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolHandler]); + + scheme = "mailto"; + allowPort = false; + + newChannel(uri, loadInfo) { + // Create an empty pipe to get an inputStream. + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 0, 0); + pipe.outputStream.close(); + + // Create a channel so that we can set contentType onto it. + let streamChannel = Cc[ + "@mozilla.org/network/input-stream-channel;1" + ].createInstance(Ci.nsIInputStreamChannel); + streamChannel.setURI(uri); + streamChannel.contentStream = pipe.inputStream; + + let channel = streamChannel.QueryInterface(Ci.nsIChannel); + // With this set, a nsIContentHandler instance will take over to open a + // compose window. + channel.contentType = "application/x-mailto"; + channel.loadInfo = loadInfo; + return channel; + } +} diff --git a/comm/mailnews/compose/src/MessageSend.jsm b/comm/mailnews/compose/src/MessageSend.jsm new file mode 100644 index 0000000000..914694fa79 --- /dev/null +++ b/comm/mailnews/compose/src/MessageSend.jsm @@ -0,0 +1,1434 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MessageSend"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + MailUtils: "resource:///modules/MailUtils.jsm", + jsmime: "resource:///modules/jsmime.jsm", + MimeMessage: "resource:///modules/MimeMessage.jsm", + MsgUtils: "resource:///modules/MimeMessageUtils.jsm", +}); + +// nsMsgKey_None from MailNewsTypes.h. +const nsMsgKey_None = 0xffffffff; + +/** + * A class to manage sending processes. + * + * @implements {nsIMsgSend} + * @implements {nsIWebProgressListener} + */ +class MessageSend { + QueryInterface = ChromeUtils.generateQI([ + "nsIMsgSend", + "nsIWebProgressListener", + ]); + classID = Components.ID("{028b9c1e-8d0a-4518-80c2-842e07846eaa}"); + + async createAndSendMessage( + editor, + userIdentity, + accountKey, + compFields, + isDigest, + dontDeliver, + deliverMode, + msgToReplace, + bodyType, + body, + parentWindow, + progress, + listener, + smtpPassword, + originalMsgURI, + compType + ) { + this._userIdentity = userIdentity; + this._accountKey = accountKey || this._accountKeyForIdentity(userIdentity); + this._compFields = compFields; + this._dontDeliver = dontDeliver; + this._deliverMode = deliverMode; + this._msgToReplace = msgToReplace; + this._sendProgress = progress; + this._smtpPassword = smtpPassword; + this._sendListener = listener; + this._parentWindow = parentWindow; + this._originalMsgURI = originalMsgURI; + this._shouldRemoveMessageFile = true; + + this._sendReport = Cc[ + "@mozilla.org/messengercompose/sendreport;1" + ].createInstance(Ci.nsIMsgSendReport); + this._composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + + // Initialize the error reporting mechanism. + this.sendReport.reset(); + this.sendReport.deliveryMode = deliverMode; + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMailInformation") + ); + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage; + + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMessage") + ); + + this._fcc = lazy.MsgUtils.getFcc( + userIdentity, + compFields, + originalMsgURI, + compType + ); + let { embeddedAttachments, embeddedObjects } = + this._gatherEmbeddedAttachments(editor); + + let bodyText = this._getBodyFromEditor(editor) || body; + // Convert to a binary string. This is because MimeMessage requires it and: + // 1. An attachment content is BinaryString. + // 2. Body text and attachment contents are handled in the same way by + // MimeEncoder to pick encoding and encode. + bodyText = lazy.jsmime.mimeutils.typedArrayToString( + new TextEncoder().encode(bodyText) + ); + + this._restoreEditorContent(embeddedObjects); + this._message = new lazy.MimeMessage( + userIdentity, + compFields, + this._fcc, + bodyType, + bodyText, + deliverMode, + originalMsgURI, + compType, + embeddedAttachments, + this.sendReport + ); + + this._messageKey = nsMsgKey_None; + + this._setStatusMessage( + this._composeBundle.GetStringFromName("creatingMailMessage") + ); + lazy.MsgUtils.sendLogger.debug("Creating message file"); + let messageFile; + try { + // Create a local file from MimeMessage, then pass it to _deliverMessage. + messageFile = await this._message.createMessageFile(); + } catch (e) { + lazy.MsgUtils.sendLogger.error(e); + let errorMsg = ""; + if (e.result == lazy.MsgUtils.NS_MSG_ERROR_ATTACHING_FILE) { + errorMsg = this._composeBundle.formatStringFromName( + "errorAttachingFile", + [e.data.name || e.data.url] + ); + } + this.fail(e.result || Cr.NS_ERROR_FAILURE, errorMsg); + this.notifyListenerOnStopSending(null, e.result, null, null); + return null; + } + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMessageDone") + ); + lazy.MsgUtils.sendLogger.debug("Message file created"); + return this._deliverMessage(messageFile); + } + + sendMessageFile( + userIdentity, + accountKey, + compFields, + messageFile, + deleteSendFileOnCompletion, + digest, + deliverMode, + msgToReplace, + listener, + statusFeedback, + smtpPassword + ) { + this._userIdentity = userIdentity; + this._accountKey = accountKey || this._accountKeyForIdentity(userIdentity); + this._compFields = compFields; + this._deliverMode = deliverMode; + this._msgToReplace = msgToReplace; + this._smtpPassword = smtpPassword; + this._sendListener = listener; + this._statusFeedback = statusFeedback; + this._shouldRemoveMessageFile = deleteSendFileOnCompletion; + + this._sendReport = Cc[ + "@mozilla.org/messengercompose/sendreport;1" + ].createInstance(Ci.nsIMsgSendReport); + this._composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + + // Initialize the error reporting mechanism. + this.sendReport.reset(); + this.sendReport.deliveryMode = deliverMode; + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMailInformation") + ); + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage; + + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMessage") + ); + + this._fcc = lazy.MsgUtils.getFcc( + userIdentity, + compFields, + null, + Ci.nsIMsgCompType.New + ); + + // nsMsgKey_None from MailNewsTypes.h. + this._messageKey = 0xffffffff; + + return this._deliverMessage(messageFile); + } + + // @see nsIMsgSend + createRFC822Message( + userIdentity, + compFields, + bodyType, + bodyText, + isDraft, + attachedFiles, + embeddedObjects, + listener + ) { + this._userIdentity = userIdentity; + this._compFields = compFields; + this._dontDeliver = true; + this._sendListener = listener; + + this._sendReport = Cc[ + "@mozilla.org/messengercompose/sendreport;1" + ].createInstance(Ci.nsIMsgSendReport); + this._composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + + // Initialize the error reporting mechanism. + this.sendReport.reset(); + let deliverMode = isDraft + ? Ci.nsIMsgSend.nsMsgSaveAsDraft + : Ci.nsIMsgSend.nsMsgDeliverNow; + this.sendReport.deliveryMode = deliverMode; + + // Convert nsIMsgAttachedFile[] to nsIMsgAttachment[] + for (let file of attachedFiles) { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = file.realName; + attachment.url = file.origUrl.spec; + attachment.contentType = file.type; + compFields.addAttachment(attachment); + } + + // Convert nsIMsgEmbeddedImageData[] to nsIMsgAttachment[] + let embeddedAttachments = embeddedObjects.map(obj => { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = obj.name; + attachment.contentId = obj.cid; + attachment.url = obj.uri.spec; + return attachment; + }); + + this._message = new lazy.MimeMessage( + userIdentity, + compFields, + null, + bodyType, + bodyText, + deliverMode, + null, + Ci.nsIMsgCompType.New, + embeddedAttachments, + this.sendReport + ); + + this._messageKey = nsMsgKey_None; + + // Create a local file from MimeMessage, then pass it to _deliverMessage. + this._message + .createMessageFile() + .then(messageFile => this._deliverMessage(messageFile)); + } + + // nsIWebProgressListener. + onLocationChange(webProgress, request, location, flags) {} + onProgressChange( + webProgress, + request, + curSelfProgress, + maxSelfProgress, + curTotalProgress, + maxTotalProgress + ) {} + onStatusChange(webProgress, request, status, message) {} + onSecurityChange(webProgress, request, state) {} + onContentBlockingEvent(webProgress, request, event) {} + onStateChange(webProgress, request, stateFlags, status) { + if ( + stateFlags & Ci.nsIWebProgressListener.STATE_STOP && + !Components.isSuccessCode(status) + ) { + lazy.MsgUtils.sendLogger.debug("onStateChange with failure. Aborting."); + this._isRetry = false; + this.abort(); + } + } + + abort() { + if (this._aborting) { + return; + } + this._aborting = true; + if (this._smtpRequest?.value) { + this._smtpRequest.value.cancel(Cr.NS_ERROR_ABORT); + this._smtpRequest = null; + } + if (this._msgCopy) { + MailServices.copy.notifyCompletion( + this._copyFile, + this._msgCopy.dstFolder, + Cr.NS_ERROR_ABORT + ); + } else { + // If already in the fcc step, notifyListenerOnStopCopy will do the clean up. + this._cleanup(); + } + if (!this._failed) { + // Emit stopsending event if the sending is cancelled by user, so that + // listeners can do necessary clean up, e.g. reset the sending button. + this.notifyListenerOnStopSending(null, Cr.NS_ERROR_ABORT, null, null); + } + this._aborting = false; + } + + fail(exitCode, errorMsg) { + this._failed = true; + if (!Components.isSuccessCode(exitCode) && exitCode != Cr.NS_ERROR_ABORT) { + lazy.MsgUtils.sendLogger.error( + `Sending failed; ${errorMsg}, exitCode=${exitCode}, originalMsgURI=${this._originalMsgURI}` + ); + this._sendReport.setError( + Ci.nsIMsgSendReport.process_Current, + exitCode, + false + ); + if (errorMsg) { + this._sendReport.setMessage( + Ci.nsIMsgSendReport.process_Current, + errorMsg, + false + ); + } + exitCode = this._sendReport.displayReport(this._parentWindow, true, true); + } + this.abort(); + + return exitCode; + } + + getPartForDomIndex(domIndex) { + throw Components.Exception( + "getPartForDomIndex not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + getProgress() { + return this._sendProgress; + } + + /** + * NOTE: This is a copy of the C++ code, msgId and msgSize are only + * placeholders. Maybe refactor this after nsMsgSend is gone. + */ + notifyListenerOnStartSending(msgId, msgSize) { + lazy.MsgUtils.sendLogger.debug("notifyListenerOnStartSending"); + if (this._sendListener) { + this._sendListener.onStartSending(msgId, msgSize); + } + } + + notifyListenerOnStartCopy() { + lazy.MsgUtils.sendLogger.debug("notifyListenerOnStartCopy"); + if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) { + this._sendListener.OnStartCopy(); + } + } + + notifyListenerOnProgressCopy(progress, progressMax) { + lazy.MsgUtils.sendLogger.debug("notifyListenerOnProgressCopy"); + if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) { + this._sendListener.OnProgress(progress, progressMax); + } + } + + notifyListenerOnStopCopy(status) { + lazy.MsgUtils.sendLogger.debug( + `notifyListenerOnStopCopy; status=${status}` + ); + this._msgCopy = null; + + if (!this._isRetry) { + let statusMsgEntry = Components.isSuccessCode(status) + ? "copyMessageComplete" + : "copyMessageFailed"; + this._setStatusMessage( + this._composeBundle.GetStringFromName(statusMsgEntry) + ); + } else if (Components.isSuccessCode(status)) { + // We got here via retry and the save to sent, drafts or template + // succeeded so take down our progress dialog. We don't need it any more. + this._sendProgress.unregisterListener(this); + this._sendProgress.closeProgressDialog(false); + this._isRetry = false; + } + + if (!Components.isSuccessCode(status)) { + let localFoldersAccountName = + MailServices.accounts.localFoldersServer.prettyName; + let folder = lazy.MailUtils.getOrCreateFolder(this._folderUri); + let accountName = folder?.server.prettyName; + if (!this._fcc || !localFoldersAccountName || !accountName) { + this.fail(Cr.NS_OK, null); + return; + } + + let params = [folder.name, accountName, localFoldersAccountName]; + let promptMsg; + switch (this._deliverMode) { + case Ci.nsIMsgSend.nsMsgDeliverNow: + case Ci.nsIMsgSend.nsMsgSendUnsent: + promptMsg = this._composeBundle.formatStringFromName( + "promptToSaveSentLocally2", + params + ); + break; + case Ci.nsIMsgSend.nsMsgSaveAsDraft: + promptMsg = this._composeBundle.formatStringFromName( + "promptToSaveDraftLocally2", + params + ); + break; + case Ci.nsIMsgSend.nsMsgSaveAsTemplate: + promptMsg = this._composeBundle.formatStringFromName( + "promptToSaveTemplateLocally2", + params + ); + break; + } + if (promptMsg) { + let showCheckBox = { value: false }; + let buttonFlags = + Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING + + Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE + + Ci.nsIPrompt.BUTTON_POS_2 * Ci.nsIPrompt.BUTTON_TITLE_SAVE; + let dialogTitle = + this._composeBundle.GetStringFromName("SaveDialogTitle"); + let buttonLabelRety = + this._composeBundle.GetStringFromName("buttonLabelRetry2"); + let buttonPressed = Services.prompt.confirmEx( + this._parentWindow, + dialogTitle, + promptMsg, + buttonFlags, + buttonLabelRety, + null, + null, + null, + showCheckBox + ); + if (buttonPressed == 0) { + // retry button clicked + // Check we have a progress dialog. + if ( + this._sendProgress.processCanceledByUser && + Services.prefs.getBoolPref("mailnews.show_send_progress") + ) { + let progress = Cc[ + "@mozilla.org/messenger/progress;1" + ].createInstance(Ci.nsIMsgProgress); + + let params = Cc[ + "@mozilla.org/messengercompose/composeprogressparameters;1" + ].createInstance(Ci.nsIMsgComposeProgressParams); + params.subject = this._parentWindow.gMsgCompose.compFields.subject; + params.deliveryMode = this._deliverMode; + + progress.openProgressDialog( + this._parentWindow, + this._sendProgress.msgWindow, + "chrome://messenger/content/messengercompose/sendProgress.xhtml", + false, + params + ); + + progress.onStateChange( + null, + null, + Ci.nsIWebProgressListener.STATE_START, + Cr.NS_OK + ); + + // We want to hear when this is cancelled. + progress.registerListener(this); + + this._sendProgress = progress; + this._isRetry = true; + } + // Ensure statusFeedback is set so progress percent bargraph occurs. + this._sendProgress.msgWindow.statusFeedback = this._sendProgress; + + this._mimeDoFcc(); + return; + } else if (buttonPressed == 2) { + try { + // Try to save to Local Folders/<account name>. Pass null to save + // to local folders and not the configured fcc. + this._mimeDoFcc(null, true, Ci.nsIMsgSend.nsMsgDeliverNow); + return; + } catch (e) { + Services.prompt.alert( + this._parentWindow, + null, + this._composeBundle.GetStringFromName("saveToLocalFoldersFailed") + ); + } + } + } + this.fail(Cr.NS_OK, null); + } + + if ( + !this._fcc2Handled && + this._messageKey != nsMsgKey_None && + [Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes( + this._deliverMode + ) + ) { + try { + this._filterSentMessage(); + } catch (e) { + this.onStopOperation(e.result); + } + return; + } + + this._doFcc2(); + } + + notifyListenerOnStopSending(msgId, status, msg, returnFile) { + lazy.MsgUtils.sendLogger.debug( + `notifyListenerOnStopSending; status=${status}` + ); + try { + this._sendListener?.onStopSending(msgId, status, msg, returnFile); + } catch (e) {} + } + + notifyListenerOnTransportSecurityError(msgId, status, secInfo, location) { + lazy.MsgUtils.sendLogger.debug( + `notifyListenerOnTransportSecurityError; status=${status}, location=${location}` + ); + if (!this._sendListener) { + return; + } + try { + this._sendListener.onTransportSecurityError( + msgId, + status, + secInfo, + location + ); + } catch (e) {} + } + + /** + * Called by nsIMsgFilterService. + */ + onStopOperation(status) { + lazy.MsgUtils.sendLogger.debug(`onStopOperation; status=${status}`); + if (Components.isSuccessCode(status)) { + this._setStatusMessage( + this._composeBundle.GetStringFromName("filterMessageComplete") + ); + } else { + this._setStatusMessage( + this._composeBundle.GetStringFromName("filterMessageFailed") + ); + Services.prompt.alert( + this._parentWindow, + null, + this._composeBundle.GetStringFromName("errorFilteringMsg") + ); + } + + this._doFcc2(); + } + + /** + * Handle the exit code of message delivery. + * + * @param {nsIURI} url - The delivered message uri. + * @param {boolean} isNewsDelivery - The message was delivered to newsgroup. + * @param {nsreault} exitCode - The exit code of message delivery. + */ + _deliveryExitProcessing(url, isNewsDelivery, exitCode) { + lazy.MsgUtils.sendLogger.debug( + `Delivery exit processing; exitCode=${exitCode}` + ); + if (!Components.isSuccessCode(exitCode)) { + let isNSSError = false; + let errorName = lazy.MsgUtils.getErrorStringName(exitCode); + let errorMsg; + if ( + [ + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER, + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_REFUSED, + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED, + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT, + lazy.MsgUtils.NS_ERROR_SMTP_PASSWORD_UNDEFINED, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_GSSAPI, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT, + lazy.MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS, + ].includes(exitCode) + ) { + errorMsg = lazy.MsgUtils.formatStringWithSMTPHostName( + this._userIdentity, + this._composeBundle, + errorName + ); + } else { + let nssErrorsService = Cc[ + "@mozilla.org/nss_errors_service;1" + ].getService(Ci.nsINSSErrorsService); + try { + // This is a server security issue as determined by the Mozilla + // platform. To the Mozilla security message string, appended a string + // having additional information with the server name encoded. + errorMsg = nssErrorsService.getErrorMessage(exitCode); + errorMsg += + "\n" + + lazy.MsgUtils.formatStringWithSMTPHostName( + this._userIdentity, + this._composeBundle, + "smtpSecurityIssue" + ); + isNSSError = true; + } catch (e) { + if (url.errorMessage) { + // url.errorMessage is an already localized message, usually + // combined with the error message from SMTP server. + errorMsg = url.errorMessage; + } else if (errorName != "sendFailed") { + // Not the default string. A mailnews error occurred that does not + // require the server name to be encoded. Just print the descriptive + // string. + errorMsg = this._composeBundle.GetStringFromName(errorName); + } else { + errorMsg = this._composeBundle.GetStringFromName( + "sendFailedUnexpected" + ); + // nsIStringBundle.formatStringFromName doesn't work with %X. + errorMsg.replace("%X", `0x${exitCode.toString(16)}`); + errorMsg = + "\n" + + lazy.MsgUtils.formatStringWithSMTPHostName( + this._userIdentity, + this._composeBundle, + "smtpSendFailedUnknownReason" + ); + } + } + } + if (isNSSError) { + let u = url.QueryInterface(Ci.nsIMsgMailNewsUrl); + this.notifyListenerOnTransportSecurityError( + null, + exitCode, + u.failedSecInfo, + u.asciiHostPort + ); + } + this.notifyListenerOnStopSending(null, exitCode, null, null); + this.fail(exitCode, errorMsg); + return; + } + + if ( + isNewsDelivery && + (this._compFields.to || this._compFields.cc || this._compFields.bcc) + ) { + this._deliverAsMail(); + return; + } + + this.notifyListenerOnStopSending( + this._compFields.messageId, + exitCode, + null, + null + ); + + this._doFcc(); + } + + sendDeliveryCallback(url, isNewsDelivery, exitCode) { + if (isNewsDelivery) { + if ( + !Components.isSuccessCode(exitCode) && + exitCode != Cr.NS_ERROR_ABORT && + !lazy.MsgUtils.isMsgError(exitCode) + ) { + exitCode = lazy.MsgUtils.NS_ERROR_POST_FAILED; + } + return this._deliveryExitProcessing(url, isNewsDelivery, exitCode); + } + if (!Components.isSuccessCode(exitCode)) { + switch (exitCode) { + case Cr.NS_ERROR_UNKNOWN_HOST: + case Cr.NS_ERROR_UNKNOWN_PROXY_HOST: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER; + break; + case Cr.NS_ERROR_CONNECTION_REFUSED: + case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_REFUSED; + break; + case Cr.NS_ERROR_NET_INTERRUPT: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED; + break; + case Cr.NS_ERROR_NET_TIMEOUT: + case Cr.NS_ERROR_NET_RESET: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT; + break; + default: + break; + } + } + return this._deliveryExitProcessing(url, isNewsDelivery, exitCode); + } + + get folderUri() { + return this._folderUri; + } + + /** + * @type {nsMsgKey} + */ + set messageKey(key) { + this._messageKey = key; + } + + /** + * @type {nsMsgKey} + */ + get messageKey() { + return this._messageKey; + } + + get sendReport() { + return this._sendReport; + } + + _setStatusMessage(msg) { + if (this._sendProgress) { + this._sendProgress.onStatusChange(null, null, Cr.NS_OK, msg); + } + } + + /** + * Deliver a message. + * + * @param {nsIFile} file - The message file to deliver. + */ + async _deliverMessage(file) { + if (this._dontDeliver) { + this.notifyListenerOnStopSending(null, Cr.NS_OK, null, file); + return; + } + + this._messageFile = file; + if ( + [ + Ci.nsIMsgSend.nsMsgQueueForLater, + Ci.nsIMsgSend.nsMsgDeliverBackground, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + ].includes(this._deliverMode) + ) { + await this._mimeDoFcc(); + return; + } + + let warningSize = Services.prefs.getIntPref( + "mailnews.message_warning_size" + ); + if (warningSize > 0 && file.fileSize > warningSize) { + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + let msg = this._composeBundle.formatStringFromName( + "largeMessageSendWarning", + [messenger.formatFileSize(file.fileSize)] + ); + if (!Services.prompt.confirm(this._parentWindow, null, msg)) { + this.fail(lazy.MsgUtils.NS_ERROR_BUT_DONT_SHOW_ALERT, msg); + throw Components.Exception( + "Cancelled sending large message", + Cr.NS_ERROR_FAILURE + ); + } + } + + this._deliveryFile = await this._createDeliveryFile(); + if (this._compFields.newsgroups) { + this._deliverAsNews(); + return; + } + await this._deliverAsMail(); + } + + /** + * Strip Bcc header, create the file to be actually delivered. + * + * @returns {nsIFile} + */ + async _createDeliveryFile() { + if (!this._compFields.bcc) { + return this._messageFile; + } + let deliveryFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + deliveryFile.append("nsemail.tmp"); + deliveryFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + let content = await IOUtils.read(this._messageFile.path); + let bodyIndex = content.findIndex( + (el, index) => + // header and body are separated by \r\n\r\n + el == 13 && + content[index + 1] == 10 && + content[index + 2] == 13 && + content[index + 3] == 10 + ); + let header = new TextDecoder("UTF-8").decode(content.slice(0, bodyIndex)); + let lastLinePruned = false; + let headerToWrite = ""; + for (let line of header.split("\r\n")) { + if (line.startsWith("Bcc") || (line.startsWith(" ") && lastLinePruned)) { + lastLinePruned = true; + continue; + } + lastLinePruned = false; + headerToWrite += `${line}\r\n`; + } + let encodedHeader = new TextEncoder().encode(headerToWrite); + // Prevent extra \r\n, which was already added to the last head line. + let body = content.slice(bodyIndex + 2); + let combinedContent = new Uint8Array(encodedHeader.length + body.length); + combinedContent.set(encodedHeader); + combinedContent.set(body, encodedHeader.length); + await IOUtils.write(deliveryFile.path, combinedContent); + return deliveryFile; + } + + /** + * Create the file to be copied to the Sent folder, add X-Mozilla-Status and + * X-Mozilla-Status2 if needed. + * + * @returns {nsIFile} + */ + async _createCopyFile() { + if (!this._folderUri.startsWith("mailbox:")) { + return this._messageFile; + } + + // Add a `From - Date` line, so that nsLocalMailFolder.cpp won't add a + // dummy envelope. The date string will be parsed by PR_ParseTimeString. + // TODO: this should not be added to Maildir, see bug 1686852. + let contentToWrite = `From - ${new Date().toUTCString()}\r\n`; + let xMozillaStatus = lazy.MsgUtils.getXMozillaStatus(this._deliverMode); + let xMozillaStatus2 = lazy.MsgUtils.getXMozillaStatus2(this._deliverMode); + if (xMozillaStatus) { + contentToWrite += `X-Mozilla-Status: ${xMozillaStatus}\r\n`; + } + if (xMozillaStatus2) { + contentToWrite += `X-Mozilla-Status2: ${xMozillaStatus2}\r\n`; + } + + // Create a separate copy file when there are extra headers. + let copyFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + copyFile.append("nscopy.tmp"); + copyFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + await IOUtils.writeUTF8(copyFile.path, contentToWrite); + await IOUtils.write( + copyFile.path, + await IOUtils.read(this._messageFile.path), + { + mode: "append", + } + ); + return copyFile; + } + + /** + * Start copy operation according to this._fcc value. + */ + async _doFcc() { + if (!this._fcc || !lazy.MsgUtils.canSaveToFolder(this._fcc)) { + this.notifyListenerOnStopCopy(Cr.NS_OK); + return; + } + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Copy; + this._mimeDoFcc(this._fcc, false, Ci.nsIMsgSend.nsMsgDeliverNow); + } + + /** + * Copy a message to a folder, or fallback to a folder depending on pref and + * deliverMode, usually Drafts/Sent. + * + * @param {string} [fccHeader=this._fcc] - The target folder uri to copy the + * message to. + * @param {boolean} [throwOnError=false] - By default notifyListenerOnStopCopy + * is called on error. When throwOnError is true, the caller can handle the + * error by itself. + * @param {nsMsgDeliverMode} [deliverMode=this._deliverMode] - The deliver mode. + */ + async _mimeDoFcc( + fccHeader = this._fcc, + throwOnError = false, + deliverMode = this._deliverMode + ) { + let folder; + let folderUri; + if (fccHeader) { + folder = lazy.MailUtils.getExistingFolder(fccHeader); + } + if ( + [Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes( + deliverMode + ) && + folder + ) { + this._folderUri = fccHeader; + } else if (fccHeader == null) { + // Set fcc_header to a special folder in Local Folders "account" since can't + // save to Sent mbox, typically because imap connection is down. This + // folder is created if it doesn't yet exist. + let rootFolder = MailServices.accounts.localFoldersServer.rootMsgFolder; + folderUri = rootFolder.URI + "/"; + + // Now append the special folder name folder to the local folder uri. + if ( + [ + Ci.nsIMsgSend.nsMsgDeliverNow, + Ci.nsIMsgSend.nsMsgSendUnsent, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + ].includes(this._deliverMode) + ) { + // Typically, this appends "Sent-", "Drafts-" or "Templates-" to folder + // and then has the account name appended, e.g., .../Sent-MyImapAccount. + let folder = lazy.MailUtils.getOrCreateFolder(this._folderUri); + folderUri += folder.name + "-"; + } + if (this._fcc) { + // Get the account name where the "save to" failed. + let accountName = lazy.MailUtils.getOrCreateFolder(this._fcc).server + .prettyName; + + // Now append the imap account name (escaped) to the folder uri. + folderUri += accountName; + this._folderUri = folderUri; + } + } else { + this._folderUri = lazy.MsgUtils.getMsgFolderURIFromPrefs( + this._userIdentity, + this._deliverMode + ); + if ( + (this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft && + this._compFields.draftId) || + (this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate && + this._compFields.templateId) + ) { + // Turn the draft/template ID into a folder URI string. + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + try { + // This can fail if the user renames/removed/moved the folder. + folderUri = messenger.msgHdrFromURI( + this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft + ? this._compFields.draftId + : this._compFields.templateId + ).folder.URI; + } catch (ex) { + console.warn(ex); + } + // Only accept it if it's a subfolder of the identity's draft/template folder. + if (folderUri?.startsWith(this._folderUri)) { + this._folderUri = folderUri; + } + } + } + lazy.MsgUtils.sendLogger.debug( + `Processing fcc; folderUri=${this._folderUri}` + ); + + this._msgCopy = Cc[ + "@mozilla.org/messengercompose/msgcopy;1" + ].createInstance(Ci.nsIMsgCopy); + this._copyFile = await this._createCopyFile(); + lazy.MsgUtils.sendLogger.debug("fcc file created"); + + // Notify nsMsgCompose about the saved folder. + if (this._sendListener) { + this._sendListener.onGetDraftFolderURI( + this._compFields.messageId, + this._folderUri + ); + } + folder = lazy.MailUtils.getOrCreateFolder(this._folderUri); + let statusMsg = this._composeBundle.formatStringFromName( + "copyMessageStart", + [folder?.name || "?"] + ); + this._setStatusMessage(statusMsg); + lazy.MsgUtils.sendLogger.debug("startCopyOperation"); + try { + this._msgCopy.startCopyOperation( + this._userIdentity, + this._copyFile, + this._deliverMode, + this, + this._folderUri, + this._msgToReplace + ); + } catch (e) { + lazy.MsgUtils.sendLogger.warn( + `startCopyOperation failed with ${e.result}` + ); + if (throwOnError) { + throw Components.Exception("startCopyOperation failed", e.result); + } + this.notifyListenerOnStopCopy(e.result); + } + } + + /** + * Handle the fcc2 field. Then notify OnStopCopy and clean up. + */ + _doFcc2() { + // Handle fcc2 only once. + if (!this._fcc2Handled && this._compFields.fcc2) { + lazy.MsgUtils.sendLogger.debug("Processing fcc2"); + this._fcc2Handled = true; + this._mimeDoFcc( + this._compFields.fcc2, + false, + Ci.nsIMsgSend.nsMsgDeliverNow + ); + return; + } + + // NOTE: When nsMsgCompose receives OnStopCopy, it will release nsIMsgSend + // instance and close the compose window, which prevents the Promise from + // resolving in MsgComposeCommands.js. Use setTimeout to work around it. + lazy.setTimeout(() => { + try { + if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) { + this._sendListener.OnStopCopy(0); + } + } catch (e) { + // Ignore the return value of OnStopCopy. Non-zero nsresult will throw + // when going through XPConnect. In this case, we don't care about it. + console.warn( + `OnStopCopy failed with 0x${e.result.toString(16)}\n${e.stack}` + ); + } + this._cleanup(); + }); + } + + /** + * Run filters on the just sent message. + */ + _filterSentMessage() { + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Filter; + let folder = lazy.MailUtils.getExistingFolder(this._folderUri); + let msgHdr = folder.GetMessageHeader(this._messageKey); + let msgWindow = this._sendProgress?.msgWindow; + return MailServices.filters.applyFilters( + Ci.nsMsgFilterType.PostOutgoing, + [msgHdr], + folder, + msgWindow, + this + ); + } + + _cleanup() { + lazy.MsgUtils.sendLogger.debug("Clean up temporary files"); + if (this._copyFile && this._copyFile != this._messageFile) { + IOUtils.remove(this._copyFile.path).catch(console.error); + this._copyFile = null; + } + if (this._deliveryFile && this._deliveryFile != this._messageFile) { + IOUtils.remove(this._deliveryFile.path).catch(console.error); + this._deliveryFile = null; + } + if (this._messageFile && this._shouldRemoveMessageFile) { + IOUtils.remove(this._messageFile.path).catch(console.error); + this._messageFile = null; + } + } + + /** + * Send this._deliveryFile to smtp service. + */ + async _deliverAsMail() { + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_SMTP; + this._setStatusMessage( + this._composeBundle.GetStringFromName("sendingMessage") + ); + let recipients = [ + this._compFields.to, + this._compFields.cc, + this._compFields.bcc, + ].filter(Boolean); + this._collectAddressesToAddressBook(recipients); + let converter = Cc["@mozilla.org/messenger/mimeconverter;1"].getService( + Ci.nsIMimeConverter + ); + let encodedRecipients = encodeURIComponent( + converter.encodeMimePartIIStr_UTF8( + recipients.join(","), + true, + 0, + Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE + ) + ); + lazy.MsgUtils.sendLogger.debug( + `Delivering mail message <${this._compFields.messageId}>` + ); + let deliveryListener = new MsgDeliveryListener(this, false); + let msgStatus = + this._sendProgress instanceof Ci.nsIMsgStatusFeedback + ? this._sendProgress + : this._statusFeedback; + this._smtpRequest = {}; + // Do async call. This is necessary to ensure _smtpRequest is set so that + // cancel function can be obtained. + await MailServices.smtp.wrappedJSObject.sendMailMessage( + this._deliveryFile, + encodedRecipients, + this._userIdentity, + this._compFields.from, + this._smtpPassword, + deliveryListener, + msgStatus, + null, + this._compFields.DSN, + this._compFields.messageId, + {}, + this._smtpRequest + ); + } + + /** + * Send this._deliveryFile to nntp service. + */ + _deliverAsNews() { + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_NNTP; + lazy.MsgUtils.sendLogger.debug("Delivering news message"); + let deliveryListener = new MsgDeliveryListener(this, true); + let msgWindow; + try { + msgWindow = + this._sendProgress?.msgWindow || + MailServices.mailSession.topmostMsgWindow; + } catch (e) {} + MailServices.nntp.postMessage( + this._deliveryFile, + this._compFields.newsgroups, + this._accountKey, + deliveryListener, + msgWindow, + null + ); + } + + /** + * Collect outgoing addresses to address book. + * + * @param {string[]} recipients - Outgoing addresses including to/cc/bcc. + */ + _collectAddressesToAddressBook(recipients) { + let createCard = Services.prefs.getBoolPref( + "mail.collect_email_address_outgoing", + false + ); + + let addressCollector = Cc[ + "@mozilla.org/addressbook/services/addressCollector;1" + ].getService(Ci.nsIAbAddressCollector); + for (let recipient of recipients) { + addressCollector.collectAddress(recipient, createCard); + } + } + + /** + * Check if link text is equivalent to the href. + * + * @param {string} text - The innerHTML of a <a> element. + * @param {string} href - The href of a <a> element. + * @returns {boolean} true if text is equivalent to href. + */ + _isLinkFreeText(text, href) { + href = href.trim(); + if (href.startsWith("mailto:")) { + return this._isLinkFreeText(text, href.slice("mailto:".length)); + } + text = text.trim(); + return ( + text == href || + (text.endsWith("/") && text.slice(0, -1) == href) || + (href.endsWith("/") && href.slice(0, -1) == text) + ); + } + + /** + * Collect embedded objects as attachments. + * + * @returns {object} collected + * @returns {nsIMsgAttachment[]} collected.embeddedAttachments + * @returns {object[]} collected.embeddedObjects objects {element, url} + */ + _gatherEmbeddedAttachments(editor) { + let embeddedAttachments = []; + let embeddedObjects = []; + + if (!editor || !editor.document) { + return { embeddedAttachments, embeddedObjects }; + } + let nodes = []; + nodes.push(...editor.document.querySelectorAll("img")); + nodes.push(...editor.document.querySelectorAll("a")); + let body = editor.document.querySelector("body[background]"); + if (body) { + nodes.push(body); + } + + let urlCidCache = {}; + for (let element of nodes) { + if (element.tagName == "A" && element.href) { + if (this._isLinkFreeText(element.innerHTML, element.href)) { + // Set this special classname, which is recognized by nsIParserUtils, + // so that links are not duplicated in text/plain. + element.classList.add("moz-txt-link-freetext"); + } + } + let isImage = false; + let url; + let name; + let mozDoNotSend = element.getAttribute("moz-do-not-send"); + if (mozDoNotSend && mozDoNotSend != "false") { + // Only empty or moz-do-not-send="false" may be accepted later. + continue; + } + if (element.tagName == "BODY" && element.background) { + isImage = true; + url = element.background; + } else if (element.tagName == "IMG" && element.src) { + isImage = true; + url = element.src; + name = element.name; + } else if (element.tagName == "A" && element.href) { + url = element.href; + name = element.name; + } else { + continue; + } + let acceptObject = false; + // Before going further, check what scheme we're dealing with. Files need to + // be converted to data URLs during composition. "Attaching" means + // sending as a cid: part instead of original URL. + if (/^https?:\/\//i.test(url)) { + acceptObject = + (isImage && + Services.prefs.getBoolPref( + "mail.compose.attach_http_images", + false + )) || + mozDoNotSend == "false"; + } + if (/^(data|news|snews|nntp):/i.test(url)) { + acceptObject = true; + } + if (!acceptObject) { + continue; + } + + let cid; + if (urlCidCache[url]) { + // If an url has already been inserted as MimePart, just reuse the cid. + cid = urlCidCache[url]; + } else { + cid = lazy.MsgUtils.makeContentId( + this._userIdentity, + embeddedAttachments.length + 1 + ); + urlCidCache[url] = cid; + + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = name || lazy.MsgUtils.pickFileNameFromUrl(url); + attachment.contentId = cid; + attachment.url = url; + embeddedAttachments.push(attachment); + } + embeddedObjects.push({ + element, + url, + }); + + let newUrl = `cid:${cid}`; + if (element.tagName == "BODY") { + element.background = newUrl; + } else if (element.tagName == "IMG") { + element.src = newUrl; + } else if (element.tagName == "A") { + element.href = newUrl; + } + } + return { embeddedAttachments, embeddedObjects }; + } + + /** + * Restore embedded objects in editor to their original urls. + * + * @param {object[]} embeddedObjects - An array of embedded objects. + * @param {Element} embeddedObjects.element + * @param {string} embeddedObjects.url + */ + _restoreEditorContent(embeddedObjects) { + for (let { element, url } of embeddedObjects) { + if (element.tagName == "BODY") { + element.background = url; + } else if (element.tagName == "IMG") { + element.src = url; + } else if (element.tagName == "A") { + element.href = url; + } + } + } + + /** + * Get the message body from an editor. + * + * @param {nsIEditor} editor - The editor instance. + * @returns {string} + */ + _getBodyFromEditor(editor) { + if (!editor) { + return ""; + } + + let flags = + Ci.nsIDocumentEncoder.OutputFormatted | + Ci.nsIDocumentEncoder.OutputNoFormattingInPre | + Ci.nsIDocumentEncoder.OutputDisallowLineBreaking; + // bodyText is UTF-16 string. + let bodyText = editor.outputToString("text/html", flags); + + // No need to do conversion if forcing plain text. + if (!this._compFields.forcePlainText) { + let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService( + Ci.mozITXTToHTMLConv + ); + let csFlags = Ci.mozITXTToHTMLConv.kURLs; + if (Services.prefs.getBoolPref("mail.send_struct", false)) { + csFlags |= Ci.mozITXTToHTMLConv.kStructPhrase; + } + bodyText = cs.scanHTML(bodyText, csFlags); + } + + return bodyText; + } + + /** + * Get the first account key of an identity. + * + * @param {nsIMsgIdentity} identity - The identity. + * @returns {string} + */ + _accountKeyForIdentity(identity) { + let servers = MailServices.accounts.getServersForIdentity(identity); + return servers.length + ? MailServices.accounts.FindAccountForServer(servers[0])?.key + : null; + } +} + +/** + * A listener to be passed to the SMTP service. + * + * @implements {nsIUrlListener} + */ +class MsgDeliveryListener { + QueryInterface = ChromeUtils.generateQI(["nsIUrlListener"]); + + /** + * @param {nsIMsgSend} msgSend - Send instance to use. + * @param {boolean} isNewsDelivery - Whether this is an nntp message delivery. + */ + constructor(msgSend, isNewsDelivery) { + this._msgSend = msgSend; + this._isNewsDelivery = isNewsDelivery; + } + + OnStartRunningUrl(url) { + this._msgSend.notifyListenerOnStartSending(null, 0); + } + + OnStopRunningUrl(url, exitCode) { + lazy.MsgUtils.sendLogger.debug(`OnStopRunningUrl; exitCode=${exitCode}`); + let mailUrl = url.QueryInterface(Ci.nsIMsgMailNewsUrl); + mailUrl.UnRegisterListener(this); + + this._msgSend.sendDeliveryCallback(url, this._isNewsDelivery, exitCode); + } +} diff --git a/comm/mailnews/compose/src/MimeEncoder.jsm b/comm/mailnews/compose/src/MimeEncoder.jsm new file mode 100644 index 0000000000..ab4c60de42 --- /dev/null +++ b/comm/mailnews/compose/src/MimeEncoder.jsm @@ -0,0 +1,430 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MimeEncoder"]; + +const LINELENGTH_ENCODING_THRESHOLD = 990; +const MESSAGE_RFC822 = "message/rfc822"; + +/** + * A class to pick Content-Transfer-Encoding for a MimePart, and encode MimePart + * body accordingly. + */ +class MimeEncoder { + /** + * Create a MimeEncoder. + * + * @param {string} charset + * @param {string} contentType + * @param {boolean} forceMsgEncoding + * @param {boolean} isMainBody + * @param {string} content + */ + constructor(charset, contentType, forceMsgEncoding, isMainBody, content) { + this._charset = charset; + this._contentType = contentType.toLowerCase(); + this._forceMsgEncoding = forceMsgEncoding; + this._isMainBody = isMainBody; + this._body = content; + this._bodySize = content.length; + + // The encoding value will be used to set Content-Transfer-Encoding header + // and encode this._body. + this._encoding = ""; + + // Flags used to pick encoding. + this._highBitCount = 0; + this._unPrintableCount = 0; + this._ctrlCount = 0; + this._nullCount = 0; + this._hasCr = 0; + this._hasLf = 0; + this._hasCrLf = 0; + this._maxColumn = 0; + } + + /** + * @type {string} + */ + get encoding() { + return this._encoding; + } + + /** + * Use the combination of charset, content type and scanning this._body to + * decide what encoding it should have. + */ + pickEncoding() { + this._analyzeBody(); + + let strictlyMime = Services.prefs.getBoolPref("mail.strictly_mime"); + let needsB64 = false; + let isUsingQP = false; + + // Allow users to override our percentage-wise guess on whether + // the file is text or binary. + let forceB64 = Services.prefs.getBoolPref("mail.file_attach_binary"); + + // If the content-type is "image/" or something else known to be binary or + // several flavors of newlines are present, use base64 unless we're attaching + // a message (so that we don't get confused by newline conversions). + if ( + !this._isMainBody && + (forceB64 || + this._requiresB64() || + this._hasCr + this._hasLf + this._hasCrLf != 1) && + this._contentType != MESSAGE_RFC822 + ) { + needsB64 = true; + } else { + // Otherwise, we need to pick an encoding based on the contents of the + // document. + let encodeP = false; + + // Force quoted-printable if the sender does not allow conversion to 7bit. + if ( + this._forceMsgEncoding || + this._maxColumn > LINELENGTH_ENCODING_THRESHOLD || + (strictlyMime && this._unPrintableCount) || + this._nullCount + ) { + if ( + this._isMainBody && + this._contentType == "text/plain" && + // From rfc3676#section-4.2, Quoted-Printable encoding SHOULD NOT be + // used with Format=Flowed unless absolutely necessary. + Services.prefs.getBoolPref("mailnews.send_plaintext_flowed") + ) { + needsB64 = true; + } else { + encodeP = true; + } + } + + // MIME requires a special case that these types never be encoded. + if ( + this._contentType.startsWith("message") || + this._contentType.startsWith("multipart") + ) { + encodeP = false; + } + + let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService( + Ci.nsICharsetConverterManager + ); + let isCharsetMultiByte = false; + try { + isCharsetMultiByte = + manager.getCharsetData(this._charset, ".isMultibyte") == "true"; + } catch {} + + // If the Mail charset is multibyte, we force it to use Base64 for + // attachments. + if ( + !this._isMainBody && + this._charset && + isCharsetMultiByte && + (this._contentType.startsWith("text") || + // text/vcard synonym + this._contentType == "application/directory") + ) { + needsB64 = true; + } else if (this._charset == "ISO-2022-JP") { + this._encoding = "7bit"; + } else if (encodeP && this._unPrintableCount > this._bodySize / 10) { + // If the document contains more than 10% unprintable characters, + // then that seems like a good candidate for base64 instead of + // quoted-printable. + needsB64 = true; + } else if (encodeP) { + this._encoding = "quoted-printable"; + isUsingQP = true; + } else if (this._highBitCount > 0) { + this._encoding = "8bit"; + } else { + this._encoding = "7bit"; + } + } + + // Always base64 binary data. + if (needsB64) { + this._encoding = "base64"; + } + + // According to RFC 821 we must always have lines shorter than 998 bytes. + // To encode "long lines" use a CTE that will transmit shorter lines. + // Switch to base64 if we are not already using "quoted printable". + + // We don't do this for message/rfc822 attachments, since we can't + // change the original Content-Transfer-Encoding of the message we're + // attaching. We rely on the original message complying with RFC 821, + // if it doesn't we won't either. Not ideal. + if ( + this._contentType != MESSAGE_RFC822 && + this._maxColumn > LINELENGTH_ENCODING_THRESHOLD && + !isUsingQP + ) { + this._encoding = "base64"; + } + } + + /** + * Encode this._body according to the value of this.encoding. + */ + encode() { + let output; + if (this.encoding == "base64") { + output = this._encodeBase64(); + } else if (this.encoding == "quoted-printable") { + output = this._encodeQP(); + } else { + output = this._body.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n"); + } + if (!output.endsWith("\r\n")) { + output += "\r\n"; + } + return output; + } + + /** + * Scan this._body to set flags that will be used by pickEncoding. + */ + _analyzeBody() { + let currentColumn = 0; + let prevCharWasCr = false; + + for (let i = 0; i < this._bodySize; i++) { + let ch = this._body.charAt(i); + let charCode = this._body.charCodeAt(i); + if (charCode > 126) { + this._highBitCount++; + this._unPrintableCount++; + } else if (ch < " " && !"\t\r\n".includes(ch)) { + this._unPrintableCount++; + this._ctrlCount++; + if (ch == "\0") { + this._nullCount++; + } + } + + if ("\r\n".includes(ch)) { + if (ch == "\r") { + if (prevCharWasCr) { + this._hasCr = 1; + } else { + prevCharWasCr = true; + } + } else if (prevCharWasCr) { + if (currentColumn == 0) { + this._hasCrLf = 1; + } else { + this._hasCr = 1; + this._hasLf = 1; + } + prevCharWasCr = false; + } else { + this._hasLf = 1; + } + + if (this._maxColumn < currentColumn) { + this._maxColumn = currentColumn; + } + currentColumn = 0; + } else { + currentColumn++; + } + } + + if (this._maxColumn < currentColumn) { + this._maxColumn = currentColumn; + } + } + + /** + * Determine if base64 is required according to contentType. + */ + _requiresB64() { + if (this._contentType == "application/x-unknown-content-type") { + // Unknown types don't necessarily require encoding. (Note that + // "unknown" and "application/octet-stream" aren't the same.) + return false; + } + if ( + this._contentType.startsWith("image/") || + this._contentType.startsWith("audio/") || + this._contentType.startsWith("video/") || + this._contentType.startsWith("application/") + ) { + // The following types are application/ or image/ types that are actually + // known to contain textual data (meaning line-based, not binary, where + // CRLF conversion is desired rather than disastrous.) So, if the type + // is any of these, it does not *require* base64, and if we do need to + // encode it for other reasons, we'll probably use quoted-printable. + // But, if it's not one of these types, then we assume that any subtypes + // of the non-"text/" types are binary data, where CRLF conversion would + // corrupt it, so we use base64 right off the bat. + // The reason it's desirable to ship these as text instead of just using + // base64 all the time is mainly to preserve the readability of them for + // non-MIME users: if I mail a /bin/sh script to someone, it might not + // need to be encoded at all, so we should leave it readable if we can. + // This list of types was derived from the comp.mail.mime FAQ, section + // 10.2.2, "List of known unregistered MIME types" on 2-Feb-96. + const typesWhichAreReallyText = [ + "application/mac-binhex40", // APPLICATION_BINHEX + "application/pgp", // APPLICATION_PGP + "application/pgp-keys", + "application/x-pgp-message", // APPLICATION_PGP2 + "application/postscript", // APPLICATION_POSTSCRIPT + "application/x-uuencode", // APPLICATION_UUENCODE + "application/x-uue", // APPLICATION_UUENCODE2 + "application/uue", // APPLICATION_UUENCODE4 + "application/uuencode", // APPLICATION_UUENCODE3 + "application/sgml", + "application/x-csh", + "application/javascript", + "application/ecmascript", + "application/x-javascript", + "application/x-latex", + "application/x-macbinhex40", + "application/x-ns-proxy-autoconfig", + "application/x-www-form-urlencoded", + "application/x-perl", + "application/x-sh", + "application/x-shar", + "application/x-tcl", + "application/x-tex", + "application/x-texinfo", + "application/x-troff", + "application/x-troff-man", + "application/x-troff-me", + "application/x-troff-ms", + "application/x-troff-ms", + "application/x-wais-source", + "image/x-bitmap", + "image/x-pbm", + "image/x-pgm", + "image/x-portable-anymap", + "image/x-portable-bitmap", + "image/x-portable-graymap", + "image/x-portable-pixmap", // IMAGE_PPM + "image/x-ppm", + "image/x-xbitmap", // IMAGE_XBM + "image/x-xbm", // IMAGE_XBM2 + "image/xbm", // IMAGE_XBM3 + "image/x-xpixmap", + "image/x-xpm", + ]; + if (typesWhichAreReallyText.includes(this._contentType)) { + return false; + } + return true; + } + return false; + } + + /** + * Base64 encoding. See RFC 2045 6.8. We use the built-in `btoa`, then ensure + * line width is no more than 72. + */ + _encodeBase64() { + let encoded = btoa(this._body); + let ret = ""; + let length = encoded.length; + let i = 0; + let limit = 72; + while (true) { + if (i * limit > length) { + break; + } + ret += encoded.substr(i * limit, limit) + "\r\n"; + i++; + } + return ret; + } + + /** + * Quoted-printable encoding. See RFC 2045 6.7. + */ + _encodeQP() { + let currentColumn = 0; + let hexdigits = "0123456789ABCDEF"; + let white = false; + let out = ""; + + function encodeChar(ch) { + let charCode = ch.charCodeAt(0); + let ret = "="; + ret += hexdigits[charCode >> 4]; + ret += hexdigits[charCode & 0xf]; + return ret; + } + + for (let i = 0; i < this._bodySize; i++) { + let ch = this._body.charAt(i); + let charCode = this._body.charCodeAt(i); + if (ch == "\r" || ch == "\n") { + // If it's CRLF, swallow two chars instead of one. + if (i + 1 < this._bodySize && ch == "\r" && this._body[i + 1] == "\n") { + i++; + } + + // Whitespace cannot be allowed to occur at the end of the line, so we + // back up and replace the whitespace with its code. + if (white) { + let whiteChar = out.slice(-1); + out = out.slice(0, -1); + out += encodeChar(whiteChar); + } + + // Now write out the newline. + out += "\r"; + out += "\n"; + white = false; + currentColumn = 0; + } else if ( + currentColumn == 0 && + (ch == "." || + (ch == "F" && + (i >= this._bodySize - 1 || this._body[i + 1] == "r") && + (i >= this._bodySize - 2 || this._body[i + 2] == "o") && + (i >= this._bodySize - 3 || this._body[i + 3] == "m") && + (i >= this._bodySize - 4 || this._body[i + 4] == " "))) + ) { + // Just to be SMTP-safe, if "." appears in column 0, encode it. + // If this line begins with "From " (or it could but we don't have enough + // data in the buffer to be certain), encode the 'F' in hex to avoid + // potential problems with BSD mailbox formats. + white = false; + out += encodeChar(ch); + currentColumn += 3; + } else if ( + (charCode >= 33 && charCode <= 60) || + (charCode >= 62 && charCode <= 126) + ) { + // Printable characters except for '=' + white = false; + out += ch; + currentColumn++; + } else if (ch == " " || ch == "\t") { + // Whitespace + white = true; + out += ch; + currentColumn++; + } else { + white = false; + out += encodeChar(ch); + currentColumn += 3; + } + + if (currentColumn >= 73) { + // Soft line break for readability + out += "=\r\n"; + white = false; + currentColumn = 0; + } + } + + return out; + } +} diff --git a/comm/mailnews/compose/src/MimeMessage.jsm b/comm/mailnews/compose/src/MimeMessage.jsm new file mode 100644 index 0000000000..9423e84004 --- /dev/null +++ b/comm/mailnews/compose/src/MimeMessage.jsm @@ -0,0 +1,625 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MimeMessage"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +let { MimeMultiPart, MimePart } = ChromeUtils.import( + "resource:///modules/MimePart.jsm" +); +let { MsgUtils } = ChromeUtils.import( + "resource:///modules/MimeMessageUtils.jsm" +); +let { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm"); + +/** + * A class to create a top MimePart and write to a tmp file. It works like this: + * 1. collect top level MIME headers (_gatherMimeHeaders) + * 2. collect HTML/plain main body as MimePart[] (_gatherMainParts) + * 3. collect attachments as MimePart[] (_gatherAttachmentParts) + * 4. construct a top MimePart with above headers and MimePart[] (_initMimePart) + * 5. write the top MimePart to a tmp file (createMessageFile) + * NOTE: It's possible we will want to replace nsIMsgSend with the interfaces of + * MimeMessage. As a part of it, we will add a `send` method to this class. + */ +class MimeMessage { + /** + * Construct a MimeMessage. + * + * @param {nsIMsgIdentity} userIdentity + * @param {nsIMsgCompFields} compFields + * @param {string} fcc - The FCC header value. + * @param {string} bodyType + * @param {BinaryString} bodyText - This is ensured to be a 8-bit string, to + * be handled the same as attachment content. + * @param {nsMsgDeliverMode} deliverMode + * @param {string} originalMsgURI + * @param {MSG_ComposeType} compType + * @param {nsIMsgAttachment[]} embeddedAttachments - Usually Embedded images. + * @param {nsIMsgSendReport} sendReport - Used by _startCryptoEncapsulation. + */ + constructor( + userIdentity, + compFields, + fcc, + bodyType, + bodyText, + deliverMode, + originalMsgURI, + compType, + embeddedAttachments, + sendReport + ) { + this._userIdentity = userIdentity; + this._compFields = compFields; + this._fcc = fcc; + this._bodyType = bodyType; + this._bodyText = bodyText; + this._deliverMode = deliverMode; + this._compType = compType; + this._embeddedAttachments = embeddedAttachments; + this._sendReport = sendReport; + } + + /** + * Write a MimeMessage to a tmp file. + * + * @returns {nsIFile} + */ + async createMessageFile() { + let topPart = this._initMimePart(); + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("nsemail.eml"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + let fstream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + this._fstream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + fstream.init(file, -1, -1, 0); + this._fstream.init(fstream, 16 * 1024); + + this._composeSecure = this._getComposeSecure(); + if (this._composeSecure) { + await this._writePart(topPart); + this._composeSecure.finishCryptoEncapsulation(false, this._sendReport); + } else { + await this._writePart(topPart); + } + + this._fstream.close(); + fstream.close(); + + return file; + } + + /** + * Create a top MimePart to represent the full message. + * + * @returns {MimePart} + */ + _initMimePart() { + let { plainPart, htmlPart } = this._gatherMainParts(); + let embeddedParts = this._gatherEmbeddedParts(); + let attachmentParts = this._gatherAttachmentParts(); + + let relatedPart = htmlPart; + if (htmlPart && embeddedParts.length > 0) { + relatedPart = new MimeMultiPart("related"); + relatedPart.addPart(htmlPart); + relatedPart.addParts(embeddedParts); + } + let mainParts = [plainPart, relatedPart].filter(Boolean); + let topPart; + if (attachmentParts.length > 0) { + // Use multipart/mixed as long as there is at least one attachment. + topPart = new MimeMultiPart("mixed"); + if (plainPart && relatedPart) { + // Wrap mainParts inside a multipart/alternative MimePart. + let alternativePart = new MimeMultiPart("alternative"); + alternativePart.addParts(mainParts); + topPart.addPart(alternativePart); + } else { + topPart.addParts(mainParts); + } + topPart.addParts(attachmentParts); + } else { + if (mainParts.length > 1) { + // Mark the topPart as multipart/alternative. + topPart = new MimeMultiPart("alternative"); + } else { + topPart = new MimePart(); + } + topPart.addParts(mainParts); + } + + topPart.setHeaders(this._gatherMimeHeaders()); + + return topPart; + } + + /** + * Collect top level headers like From/To/Subject into a Map. + */ + _gatherMimeHeaders() { + let messageId = this._compFields.messageId; + if ( + !messageId && + (this._compFields.to || + this._compFields.cc || + this._compFields.bcc || + !this._compFields.newsgroups || + this._userIdentity.getBoolAttribute("generate_news_message_id")) + ) { + // Try to use the domain name of the From header to generate the message ID. We + // specifically don't use the nsIMsgIdentity associated with the account, because + // the user might have changed the address in the From header to use a different + // domain, and we don't want to leak the relationship between the domains. + const fromHdr = MailServices.headerParser.parseEncodedHeaderW( + this._compFields.from + ); + const fromAddr = fromHdr[0].email; + + // Extract the host from the address, if any, and generate a message ID from it. + // If we can't get a host for the message ID, let SMTP populate the header. + const atIndex = fromAddr.indexOf("@"); + if (atIndex >= 0) { + messageId = Cc["@mozilla.org/messengercompose/computils;1"] + .createInstance(Ci.nsIMsgCompUtils) + .msgGenerateMessageId( + this._userIdentity, + fromAddr.slice(atIndex + 1) + ); + } + + this._compFields.messageId = messageId; + } + let headers = new Map([ + ["message-id", messageId], + ["date", new Date()], + ["mime-version", "1.0"], + ]); + + if (Services.prefs.getBoolPref("mailnews.headers.sendUserAgent")) { + if (Services.prefs.getBoolPref("mailnews.headers.useMinimalUserAgent")) { + headers.set( + "user-agent", + Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandFullName") + ); + } else { + headers.set( + "user-agent", + Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ).userAgent + ); + } + } + + for (let headerName of [...this._compFields.headerNames]) { + let headerContent = this._compFields.getRawHeader(headerName); + if (headerContent) { + headers.set(headerName, headerContent); + } + } + let isDraft = [ + Ci.nsIMsgSend.nsMsgQueueForLater, + Ci.nsIMsgSend.nsMsgDeliverBackground, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + ].includes(this._deliverMode); + + let undisclosedRecipients = MsgUtils.getUndisclosedRecipients( + this._compFields, + this._deliverMode + ); + if (undisclosedRecipients) { + headers.set("to", undisclosedRecipients); + } + + if (isDraft) { + headers + .set( + "x-mozilla-draft-info", + MsgUtils.getXMozillaDraftInfo(this._compFields) + ) + .set("x-identity-key", this._userIdentity.key) + .set("fcc", this._fcc); + } + + if (messageId) { + // MDN request header requires to have MessageID header presented in the + // message in order to coorelate the MDN reports to the original message. + headers + .set( + "disposition-notification-to", + MsgUtils.getDispositionNotificationTo( + this._compFields, + this._deliverMode + ) + ) + .set( + "return-receipt-to", + MsgUtils.getReturnReceiptTo(this._compFields, this._deliverMode) + ); + } + + for (let { headerName, headerValue } of MsgUtils.getDefaultCustomHeaders( + this._userIdentity + )) { + headers.set(headerName, headerValue); + } + + let rawMftHeader = headers.get("mail-followup-to"); + // If there's already a Mail-Followup-To header, don't need to do anything. + if (!rawMftHeader) { + headers.set( + "mail-followup-to", + MsgUtils.getMailFollowupToHeader(this._compFields, this._userIdentity) + ); + } + + let rawMrtHeader = headers.get("mail-reply-to"); + // If there's already a Mail-Reply-To header, don't need to do anything. + if (!rawMrtHeader) { + headers.set( + "mail-reply-to", + MsgUtils.getMailReplyToHeader( + this._compFields, + this._userIdentity, + rawMrtHeader + ) + ); + } + + let rawPriority = headers.get("x-priority"); + if (rawPriority) { + headers.set("x-priority", MsgUtils.getXPriority(rawPriority)); + } + + let rawReferences = headers.get("references"); + if (rawReferences) { + let references = MsgUtils.getReferences(rawReferences); + // Don't reset "references" header if references is undefined. + if (references) { + headers.set("references", references); + } + headers.set("in-reply-to", MsgUtils.getInReplyTo(rawReferences)); + } + if ( + rawReferences && + [ + Ci.nsIMsgCompType.ForwardInline, + Ci.nsIMsgCompType.ForwardAsAttachment, + ].includes(this._compType) + ) { + headers.set("x-forwarded-message-id", rawReferences); + } + + let rawNewsgroups = headers.get("newsgroups"); + if (rawNewsgroups) { + let { newsgroups, newshost } = MsgUtils.getNewsgroups( + this._deliverMode, + rawNewsgroups + ); + // Don't reset "newsgroups" header if newsgroups is undefined. + if (newsgroups) { + headers.set("newsgroups", newsgroups); + } + headers.set("x-mozilla-news-host", newshost); + } + + return headers; + } + + /** + * Determine if the message should include an HTML part, a plain part or both. + * + * @returns {{plainPart: MimePart, htmlPart: MimePart}} + */ + _gatherMainParts() { + let formatFlowed = Services.prefs.getBoolPref( + "mailnews.send_plaintext_flowed" + ); + let formatParam = ""; + if (formatFlowed) { + // Set format=flowed as in RFC 2646 according to the preference. + formatParam += "; format=flowed"; + } + + let htmlPart = null; + let plainPart = null; + let parts = {}; + + if (this._bodyType === "text/html") { + htmlPart = new MimePart( + this._bodyType, + this._compFields.forceMsgEncoding, + true + ); + htmlPart.setHeader("content-type", "text/html; charset=UTF-8"); + htmlPart.bodyText = this._bodyText; + } else if (this._bodyType === "text/plain") { + plainPart = new MimePart( + this._bodyType, + this._compFields.forceMsgEncoding, + true + ); + plainPart.setHeader( + "content-type", + `text/plain; charset=UTF-8${formatParam}` + ); + plainPart.bodyText = this._bodyText; + parts.plainPart = plainPart; + } + + // Assemble a multipart/alternative message. + if ( + (this._compFields.forcePlainText || + this._compFields.useMultipartAlternative) && + plainPart === null && + htmlPart !== null + ) { + plainPart = new MimePart( + "text/plain", + this._compFields.forceMsgEncoding, + true + ); + plainPart.setHeader( + "content-type", + `text/plain; charset=UTF-8${formatParam}` + ); + // nsIParserUtils.convertToPlainText expects unicode string. + let plainUnicode = MsgUtils.convertToPlainText( + new TextDecoder().decode( + jsmime.mimeutils.stringToTypedArray(this._bodyText) + ), + formatFlowed + ); + // MimePart.bodyText should be binary string. + plainPart.bodyText = jsmime.mimeutils.typedArrayToString( + new TextEncoder().encode(plainUnicode) + ); + + parts.plainPart = plainPart; + } + + // If useMultipartAlternative is true, send multipart/alternative message. + // Otherwise, send the plainPart only. + if (htmlPart) { + if ( + (plainPart && this._compFields.useMultipartAlternative) || + !plainPart + ) { + parts.htmlPart = htmlPart; + } + } + + return parts; + } + + /** + * Collect local attachments. + * + * @returns {MimePart[]} + */ + _gatherAttachmentParts() { + let attachments = [...this._compFields.attachments]; + let cloudParts = []; + let localParts = []; + + for (let attachment of attachments) { + let part; + if (attachment.htmlAnnotation) { + part = new MimePart(); + // MimePart.bodyText should be binary string. + part.bodyText = jsmime.mimeutils.typedArrayToString( + new TextEncoder().encode(attachment.htmlAnnotation) + ); + part.setHeader("content-type", "text/html; charset=utf-8"); + + let suffix = /\.html$/i.test(attachment.name) ? "" : ".html"; + let encodedFilename = MsgUtils.rfc2231ParamFolding( + "filename", + `${attachment.name}${suffix}` + ); + part.setHeader("content-disposition", `attachment; ${encodedFilename}`); + } else { + part = new MimePart(null, this._compFields.forceMsgEncoding, false); + part.setBodyAttachment(attachment); + } + + let cloudPartHeader = MsgUtils.getXMozillaCloudPart( + this._deliverMode, + attachment + ); + if (cloudPartHeader) { + part.setHeader("x-mozilla-cloud-part", cloudPartHeader); + } + + localParts.push(part); + } + // Cloud attachments are handled before local attachments in the C++ + // implementation. We follow it here so that no need to change tests. + return cloudParts.concat(localParts); + } + + /** + * Collect embedded objects as attachments. + * + * @returns {MimePart[]} + */ + _gatherEmbeddedParts() { + return this._embeddedAttachments.map(attachment => { + let part = new MimePart(null, this._compFields.forceMsgEncoding, false); + part.setBodyAttachment(attachment, "inline", attachment.contentId); + return part; + }); + } + + /** + * If crypto encapsulation is required, returns an nsIMsgComposeSecure instance. + * + * @returns {nsIMsgComposeSecure} + */ + _getComposeSecure() { + let secureCompose = this._compFields.composeSecure; + if (!secureCompose) { + return null; + } + + if ( + this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft && + !this._userIdentity.getBoolAttribute("autoEncryptDrafts") + ) { + return null; + } + + if ( + !secureCompose.requiresCryptoEncapsulation( + this._userIdentity, + this._compFields + ) + ) { + return null; + } + return secureCompose; + } + + /** + * Pass a stream and other params to this._composeSecure to start crypto + * encapsulation. + */ + _startCryptoEncapsulation() { + let recipients = [ + this._compFields.to, + this._compFields.cc, + this._compFields.bcc, + this._compFields.newsgroups, + ] + .filter(Boolean) + .join(","); + + this._composeSecure.beginCryptoEncapsulation( + this._fstream, + recipients, + this._compFields, + this._userIdentity, + this._sendReport, + this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft + ); + this._cryptoEncapsulationStarted = true; + } + + /** + * Recursively write an MimePart and its parts to a this._fstream. + * + * @param {MimePart} curPart - The MimePart to write out. + * @param {number} [depth=0] - Nested level of a part. + */ + async _writePart(curPart, depth = 0) { + let bodyString; + try { + // `getEncodedBodyString()` returns a binary string. + bodyString = await curPart.getEncodedBodyString(); + } catch (e) { + if (e.data && /^data:/i.test(e.data.url)) { + // Invalid data uri should not prevent sending message. + return; + } + throw e; + } + + if (depth == 0 && this._composeSecure) { + // Crypto encapsulation will add a new content-type header. + curPart.deleteHeader("content-type"); + if (curPart.parts.length > 1) { + // Move child parts one layer deeper so that the message is still well + // formed after crypto encapsulation. + let newChild = new MimeMultiPart(curPart.subtype); + newChild.parts = curPart._parts; + curPart.parts = [newChild]; + } + } + + // Write out headers, there could be non-ASCII in the headers + // which we need to encode into UTF-8. + this._writeString(curPart.getHeaderString()); + + // Start crypto encapsulation if needed. + if (depth == 0 && this._composeSecure) { + this._startCryptoEncapsulation(); + } + + // Recursively write out parts. + if (curPart.parts.length) { + // single part message + if (curPart.parts.length === 1) { + await this._writePart(curPart.parts[0], depth + 1); + this._writeBinaryString(bodyString); + return; + } + + // We can safely use `_writeBinaryString()` for ASCII strings. + this._writeBinaryString("\r\n"); + if (depth == 0) { + // Current part is a top part and multipart container. + this._writeBinaryString( + "This is a multi-part message in MIME format.\r\n" + ); + } + + // multipart message + for (let part of curPart.parts) { + this._writeBinaryString(`--${curPart.separator}\r\n`); + await this._writePart(part, depth + 1); + } + this._writeBinaryString(`\r\n--${curPart.separator}--\r\n`); + if (depth > 1) { + // If more separators follow, make sure there is a blank line after + // this one. + this._writeBinaryString("\r\n"); + } + } else { + this._writeBinaryString(`\r\n`); + } + + // Ensure there is exactly one blank line after a part and before + // the boundary, and exactly one blank line between boundary lines. + // This works around bugs in other software that erroneously remove + // additional blank lines, thereby causing verification failures of + // OpenPGP or S/MIME signatures. For example see bug 1731529. + + // Write out body. + this._writeBinaryString(bodyString); + } + + /** + * Write a binary string to this._fstream. + * + * @param {BinaryString} str - The binary string to write. + */ + _writeBinaryString(str) { + this._cryptoEncapsulationStarted + ? this._composeSecure.mimeCryptoWriteBlock(str, str.length) + : this._fstream.write(str, str.length); + } + + /** + * Write a string to this._fstream. + * + * @param {string} str - The string to write. + */ + _writeString(str) { + this._writeBinaryString( + jsmime.mimeutils.typedArrayToString(new TextEncoder().encode(str)) + ); + } +} diff --git a/comm/mailnews/compose/src/MimeMessageUtils.jsm b/comm/mailnews/compose/src/MimeMessageUtils.jsm new file mode 100644 index 0000000000..65682b6d4c --- /dev/null +++ b/comm/mailnews/compose/src/MimeMessageUtils.jsm @@ -0,0 +1,1058 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MsgUtils"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm"); + +// Defined in ErrorList.h. +const NS_ERROR_MODULE_BASE_OFFSET = 69; +const NS_ERROR_MODULE_MAILNEWS = 16; + +/** + * Generate an NS_ERROR code from a MAILNEWS error code. See NS_ERROR_GENERATE + * in nsError.h and NS_MSG_GENERATE_FAILURE in nsComposeStrings.h. + * + * @param {number} code - The error code in MAILNEWS module. + * @returns {number} + */ +function generateNSError(code) { + return ( + ((1 << 31) | + ((NS_ERROR_MODULE_MAILNEWS + NS_ERROR_MODULE_BASE_OFFSET) << 16) | + code) >>> + 0 + ); +} + +/** + * Collection of helper functions for message sending process. + */ +var MsgUtils = { + /** + * Error codes defined in nsComposeStrings.h + */ + NS_MSG_UNABLE_TO_OPEN_FILE: generateNSError(12500), + NS_MSG_UNABLE_TO_OPEN_TMP_FILE: generateNSError(12501), + NS_MSG_UNABLE_TO_SAVE_TEMPLATE: generateNSError(12502), + NS_MSG_UNABLE_TO_SAVE_DRAFT: generateNSError(12503), + NS_MSG_COULDNT_OPEN_FCC_FOLDER: generateNSError(12506), + NS_MSG_NO_SENDER: generateNSError(12510), + NS_MSG_NO_RECIPIENTS: generateNSError(12511), + NS_MSG_ERROR_WRITING_FILE: generateNSError(12512), + NS_ERROR_SENDING_FROM_COMMAND: generateNSError(12514), + NS_ERROR_SENDING_DATA_COMMAND: generateNSError(12516), + NS_ERROR_SENDING_MESSAGE: generateNSError(12517), + NS_ERROR_POST_FAILED: generateNSError(12518), + NS_ERROR_SMTP_SERVER_ERROR: generateNSError(12524), + NS_MSG_UNABLE_TO_SEND_LATER: generateNSError(12525), + NS_ERROR_COMMUNICATIONS_ERROR: generateNSError(12526), + NS_ERROR_BUT_DONT_SHOW_ALERT: generateNSError(12527), + NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS: generateNSError(12529), + NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY: generateNSError(12530), + NS_ERROR_MIME_MPART_ATTACHMENT_ERROR: generateNSError(12531), + + // 12554 is taken by NS_ERROR_NNTP_NO_CROSS_POSTING. use 12555 as the next one + + // For message sending report + NS_MSG_ERROR_READING_FILE: generateNSError(12563), + + NS_MSG_ERROR_ATTACHING_FILE: generateNSError(12570), + + NS_ERROR_SMTP_GREETING: generateNSError(12572), + + NS_ERROR_SENDING_RCPT_COMMAND: generateNSError(12575), + + NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS: generateNSError(12582), + + NS_ERROR_SMTP_PASSWORD_UNDEFINED: generateNSError(12584), + NS_ERROR_SMTP_SEND_NOT_ALLOWED: generateNSError(12585), + NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED: generateNSError(12586), + NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2: generateNSError(12588), + + NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER: generateNSError(12589), + NS_ERROR_SMTP_SEND_FAILED_REFUSED: generateNSError(12590), + NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED: generateNSError(12591), + NS_ERROR_SMTP_SEND_FAILED_TIMEOUT: generateNSError(12592), + NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON: generateNSError(12593), + + NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL: generateNSError(12594), + NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL: generateNSError(12595), + NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT: generateNSError(12596), + NS_ERROR_SMTP_AUTH_FAILURE: generateNSError(12597), + NS_ERROR_SMTP_AUTH_GSSAPI: generateNSError(12598), + NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED: generateNSError(12599), + + NS_ERROR_ILLEGAL_LOCALPART: generateNSError(12601), + + NS_ERROR_CLIENTID: generateNSError(12610), + NS_ERROR_CLIENTID_PERMISSION: generateNSError(12611), + + sendLogger: console.createInstance({ + prefix: "mailnews.send", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.send.loglevel", + }), + + smtpLogger: console.createInstance({ + prefix: "mailnews.smtp", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.smtp.loglevel", + }), + + /** + * NS_IS_MSG_ERROR in msgCore.h. + * + * @param {nsresult} err - The nsresult value. + * @returns {boolean} + */ + isMsgError(err) { + return ( + (((err >> 16) - NS_ERROR_MODULE_BASE_OFFSET) & 0x1fff) == + NS_ERROR_MODULE_MAILNEWS + ); + }, + + /** + * Convert html to text to form a multipart/alternative message. The output + * depends on preference. + * + * @param {string} input - The HTML text to convert. + * @param {boolean} formatFlowed - A flag to enable OutputFormatFlowed. + * @returns {string} + */ + convertToPlainText(input, formatFlowed) { + let wrapWidth = Services.prefs.getIntPref("mailnews.wraplength", 72); + if (wrapWidth == 0 || wrapWidth > 990) { + wrapWidth = 990; + } else if (wrapWidth < 10) { + wrapWidth = 10; + } + + let flags = + Ci.nsIDocumentEncoder.OutputPersistNBSP | + Ci.nsIDocumentEncoder.OutputFormatted | + Ci.nsIDocumentEncoder.OutputDisallowLineBreaking; + if (formatFlowed) { + flags |= Ci.nsIDocumentEncoder.OutputFormatFlowed; + } + + let parserUtils = Cc["@mozilla.org/parserutils;1"].getService( + Ci.nsIParserUtils + ); + return parserUtils.convertToPlainText(input, flags, wrapWidth); + }, + + /** + * Get the list of default custom headers. + * + * @param {nsIMsgIdentity} userIdentity - User identity. + * @returns {{headerName: string, headerValue: string}[]} + */ + getDefaultCustomHeaders(userIdentity) { + // mail.identity.<id#>.headers pref is a comma separated value of pref names + // containing headers to add headers are stored in + let headerAttributes = userIdentity + .getUnicharAttribute("headers") + .split(","); + let headers = []; + for (let attr of headerAttributes) { + // mail.identity.<id#>.header.<header name> grab all the headers + let attrValue = userIdentity.getUnicharAttribute(`header.${attr}`); + if (attrValue) { + let colonIndex = attrValue.indexOf(":"); + headers.push({ + headerName: attrValue.slice(0, colonIndex), + headerValue: attrValue.slice(colonIndex + 1).trim(), + }); + } + } + return headers; + }, + + /** + * Get the fcc value. + * + * @param {nsIMsgIdentity} userIdentity - The user identity. + * @param {nsIMsgCompFields} compFields - The compose fields. + * @param {string} originalMsgURI - The original message uri, can be null. + * @param {MSG_ComposeType} compType - The compose type. + * @returns {string} + */ + getFcc(userIdentity, compFields, originalMsgURI, compType) { + // Check if the default fcc has been overridden. + let fcc = ""; + let useDefaultFcc = true; + if (compFields.fcc) { + if (compFields.fcc.startsWith("nocopy://")) { + useDefaultFcc = false; + fcc = ""; + } else { + let folder = MailUtils.getExistingFolder(compFields.fcc); + if (folder) { + useDefaultFcc = false; + fcc = compFields.fcc; + } + } + } + + // If the identity pref "fcc" is set to false, then we will not do the default + // FCC operation but still allow the override. + if (!userIdentity.doFcc) { + return fcc; + } + + // We use default FCC setting if it's not set or was set to an invalid + // folder. + if (useDefaultFcc) { + // Only check whether the user wants the message in the original message + // folder if the msgcomptype is some kind of a reply. + if ( + originalMsgURI && + [ + Ci.nsIMsgCompType.Reply, + Ci.nsIMsgCompType.ReplyAll, + Ci.nsIMsgCompType.ReplyToGroup, + Ci.nsIMsgCompType.ReplyToSender, + Ci.nsIMsgCompType.ReplyToSenderAndGroup, + Ci.nsIMsgCompType.ReplyToList, + Ci.nsIMsgCompType.ReplyWithTemplate, + ].includes(compType) + ) { + let msgHdr; + try { + msgHdr = + MailServices.messageServiceFromURI( + originalMsgURI + ).messageURIToMsgHdr(originalMsgURI); + } catch (e) { + console.warn( + `messageServiceFromURI failed for ${originalMsgURI}\n${e.stack}` + ); + } + if (msgHdr) { + let folder = msgHdr.folder; + if ( + folder && + folder.canFileMessages && + folder.server && + folder.server.getCharValue("type") != "rss" && + userIdentity.fccReplyFollowsParent + ) { + fcc = folder.URI; + useDefaultFcc = false; + } + } + } + + if (useDefaultFcc) { + let uri = this.getMsgFolderURIFromPrefs( + userIdentity, + Ci.nsIMsgSend.nsMsgDeliverNow + ); + fcc = uri == "nocopy://" ? "" : uri; + } + } + + return fcc; + }, + + canSaveToFolder(folderUri) { + let folder = MailUtils.getOrCreateFolder(folderUri); + if (folder.server) { + return folder.server.canFileMessagesOnServer; + } + return false; + }, + + /** + * Get the To header value. When we don't have disclosed recipient but only + * Bcc, use the undisclosedRecipients entry from composeMsgs.properties as the + * To header value to prevent problem with some servers. + * + * @param {nsIMsgCompFields} compFields - The compose fields. + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @returns {string} + */ + getUndisclosedRecipients(compFields, deliverMode) { + // Newsgroups count as recipients. + let hasDisclosedRecipient = + compFields.to || compFields.cc || compFields.newsgroups; + // If we are saving the message as a draft, don't bother inserting the + // undisclosed recipients field. We'll take care of that when we really send + // the message. + if ( + hasDisclosedRecipient || + [ + Ci.nsIMsgSend.nsMsgDeliverBackground, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + ].includes(deliverMode) || + !Services.prefs.getBoolPref("mail.compose.add_undisclosed_recipients") + ) { + return ""; + } + let composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + let undisclosedRecipients = composeBundle.GetStringFromName( + "undisclosedRecipients" + ); + let recipients = MailServices.headerParser.makeGroupObject( + undisclosedRecipients, + [] + ); + return recipients.toString(); + }, + + /** + * Get the Mail-Followup-To header value. + * See bug #204339 and http://cr.yp.to/proto/replyto.html for details + * + * @param {nsIMsgCompFields} compFields - The compose fields. + * @param {nsIMsgIdentity} userIdentity - The user identity. + * @returns {string} + */ + getMailFollowupToHeader(compFields, userIdentity) { + let mailLists = userIdentity.getUnicharAttribute( + "subscribed_mailing_lists" + ); + if (!mailLists || !(compFields.to || compFields.cc)) { + return ""; + } + let recipients = compFields.to; + if (recipients) { + if (compFields.cc) { + recipients += `,${compFields.cc}`; + } + } else { + recipients = compFields.cc; + } + let recipientsDedup = + MailServices.headerParser.removeDuplicateAddresses(recipients); + let recipientsWithoutMailList = + MailServices.headerParser.removeDuplicateAddresses( + recipientsDedup, + mailLists + ); + if (recipientsDedup != recipientsWithoutMailList) { + return recipients; + } + return ""; + }, + + /** + * Get the Mail-Reply-To header value. + * See bug #204339 and http://cr.yp.to/proto/replyto.html for details + * + * @param {nsIMsgCompFields} compFields - The compose fields. + * @param {nsIMsgIdentity} userIdentity - The user identity. + * @returns {string} + */ + getMailReplyToHeader(compFields, userIdentity) { + let mailLists = userIdentity.getUnicharAttribute( + "replyto_mangling_mailing_lists" + ); + if ( + !mailLists || + mailLists[0] == "*" || + !(compFields.to || compFields.cc) + ) { + return ""; + } + let recipients = compFields.to; + if (recipients) { + if (compFields.cc) { + recipients += `,${compFields.cc}`; + } + } else { + recipients = compFields.cc; + } + let recipientsDedup = + MailServices.headerParser.removeDuplicateAddresses(recipients); + let recipientsWithoutMailList = + MailServices.headerParser.removeDuplicateAddresses( + recipientsDedup, + mailLists + ); + if (recipientsDedup != recipientsWithoutMailList) { + return compFields.replyTo || compFields.from; + } + return ""; + }, + + /** + * Get the X-Mozilla-Draft-Info header value. + * + * @param {nsIMsgCompFields} compFields - The compose fields. + * @returns {string} + */ + getXMozillaDraftInfo(compFields) { + let getCompField = (property, key) => { + let value = compFields[property] ? 1 : 0; + return `${key}=${value}; `; + }; + let draftInfo = "internal/draft; "; + draftInfo += getCompField("attachVCard", "vcard"); + + let receiptValue = 0; + if (compFields.returnReceipt) { + // slight change compared to 4.x; we used to use receipt= to tell + // whether the draft/template has request for either MDN or DNS or both + // return receipt; since the DNS is out of the picture we now use the + // header type + 1 to tell whether user has requested the return receipt + receiptValue = compFields.receiptHeaderType + 1; + } + draftInfo += `receipt=${receiptValue}; `; + + draftInfo += getCompField("DSN", "DSN"); + draftInfo += "uuencode=0; "; + draftInfo += getCompField("attachmentReminder", "attachmentreminder"); + draftInfo += `deliveryformat=${compFields.deliveryFormat}`; + + return draftInfo; + }, + + /** + * Get the X-Mozilla-Cloud-Part header value. + * + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @param {nsIMsgAttachment} attachment - The cloud attachment. + * @returns {string} + */ + getXMozillaCloudPart(deliverMode, attachment) { + let value = ""; + if (attachment.sendViaCloud && attachment.contentLocation) { + value += `cloudFile; url=${attachment.contentLocation}`; + + if ( + (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft || + deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate) && + attachment.cloudFileAccountKey && + attachment.cloudPartHeaderData + ) { + value += `; provider=${attachment.cloudFileAccountKey}`; + value += `; ${this.rfc2231ParamFolding( + "data", + attachment.cloudPartHeaderData + )}`; + } + } + return value; + }, + + /** + * Get the X-Mozilla-Status header value. The header value will be used to set + * some nsMsgMessageFlags. Including the Read flag for message in a local + * folder. + * + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @returns {string} + */ + getXMozillaStatus(deliverMode) { + if ( + ![ + Ci.nsIMsgSend.nsMsgQueueForLater, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + Ci.nsIMsgSend.nsMsgDeliverNow, + Ci.nsIMsgSend.nsMsgSendUnsent, + Ci.nsIMsgSend.nsMsgDeliverBackground, + ].includes(deliverMode) + ) { + return ""; + } + let flags = 0; + if (deliverMode == Ci.nsIMsgSend.nsMsgQueueForLater) { + flags |= Ci.nsMsgMessageFlags.Queued; + } else if ( + deliverMode != Ci.nsIMsgSend.nsMsgSaveAsDraft && + deliverMode != Ci.nsIMsgSend.nsMsgDeliverBackground + ) { + flags |= Ci.nsMsgMessageFlags.Read; + } + return flags.toString(16).padStart(4, "0"); + }, + + /** + * Get the X-Mozilla-Status2 header value. The header value will be used to + * set some nsMsgMessageFlags. + * + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @returns {string} + */ + getXMozillaStatus2(deliverMode) { + if ( + ![ + Ci.nsIMsgSend.nsMsgQueueForLater, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + Ci.nsIMsgSend.nsMsgDeliverNow, + Ci.nsIMsgSend.nsMsgSendUnsent, + Ci.nsIMsgSend.nsMsgDeliverBackground, + ].includes(deliverMode) + ) { + return ""; + } + let flags = 0; + if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate) { + flags |= Ci.nsMsgMessageFlags.Template; + } else if ( + deliverMode == Ci.nsIMsgSend.nsMsgDeliverNow || + deliverMode == Ci.nsIMsgSend.nsMsgSendUnsent + ) { + flags &= ~Ci.nsMsgMessageFlags.MDNReportNeeded; + flags |= Ci.nsMsgMessageFlags.MDNReportSent; + } + return flags.toString(16).padStart(8, "0"); + }, + + /** + * Get the Disposition-Notification-To header value. + * + * @param {nsIMsgCompFields} compFields - The compose fields. + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @returns {{dnt: string, rrt: string}} + */ + getDispositionNotificationTo(compFields, deliverMode) { + if ( + compFields.returnReceipt && + deliverMode != Ci.nsIMsgSend.nsMsgSaveAsDraft && + deliverMode != Ci.nsIMsgSend.nsMsgSaveAsTemplate && + compFields.receiptHeaderType != Ci.nsIMsgMdnGenerator.eRrtType + ) { + return compFields.from; + } + return ""; + }, + + /** + * Get the Return-Receipt-To header value. + * + * @param {nsIMsgCompFields} compFields - The compose fields. + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @returns {{dnt: string, rrt: string}} + */ + getReturnReceiptTo(compFields, deliverMode) { + if ( + compFields.returnReceipt && + deliverMode != Ci.nsIMsgSend.nsMsgSaveAsDraft && + deliverMode != Ci.nsIMsgSend.nsMsgSaveAsTemplate && + compFields.receiptHeaderType != Ci.nsIMsgMdnGenerator.eDntType + ) { + return compFields.from; + } + return ""; + }, + + /** + * Get the value of X-Priority header. + * + * @param {string} rawPriority - Raw X-Priority content. + * @returns {string} + */ + getXPriority(rawPriority) { + rawPriority = rawPriority.toLowerCase(); + let priorityValue = Ci.nsMsgPriority.Default; + let priorityValueString = "0"; + let priorityName = "None"; + if (rawPriority.startsWith("1") || rawPriority.startsWith("highest")) { + priorityValue = Ci.nsMsgPriority.highest; + priorityValueString = "1"; + priorityName = "Highest"; + } else if ( + rawPriority.startsWith("2") || + // "high" must be tested after "highest". + rawPriority.startsWith("high") || + rawPriority.startsWith("urgent") + ) { + priorityValue = Ci.nsMsgPriority.high; + priorityValueString = "2"; + priorityName = "High"; + } else if ( + rawPriority.startsWith("3") || + rawPriority.startsWith("normal") + ) { + priorityValue = Ci.nsMsgPriority.normal; + priorityValueString = "3"; + priorityName = "Normal"; + } else if ( + rawPriority.startsWith("5") || + rawPriority.startsWith("lowest") + ) { + priorityValue = Ci.nsMsgPriority.lowest; + priorityValueString = "5"; + priorityName = "Lowest"; + } else if ( + rawPriority.startsWith("4") || + // "low" must be tested after "lowest". + rawPriority.startsWith("low") + ) { + priorityValue = Ci.nsMsgPriority.low; + priorityValueString = "4"; + priorityName = "Low"; + } + if (priorityValue == Ci.nsMsgPriority.Default) { + return ""; + } + return `${priorityValueString} (${priorityName})`; + }, + + /** + * Get the References header value. + * + * @param {string} references - Raw References header content. + * @returns {string} + */ + getReferences(references) { + if (references.length <= 986) { + return ""; + } + // The References header should be kept under 998 characters: if it's too + // long, trim out the earliest references to make it smaller. + let newReferences = ""; + let firstRef = references.indexOf("<"); + let secondRef = references.indexOf("<", firstRef + 1); + if (secondRef > 0) { + newReferences = references.slice(0, secondRef); + let bracket = references.indexOf( + "<", + references.length + newReferences.length - 986 + ); + if (bracket > 0) { + newReferences += references.slice(bracket); + } + } + return newReferences; + }, + + /** + * Get the In-Reply-To header value. + * + * @param {string} references - Raw References header content. + * @returns {string} + */ + getInReplyTo(references) { + // The In-Reply-To header is the last entry in the references header... + let bracket = references.lastIndexOf("<"); + if (bracket >= 0) { + return references.slice(bracket); + } + return ""; + }, + + /** + * Get the value of Newsgroups and X-Mozilla-News-Host header. + * + * @param {nsMsgDeliverMode} deliverMode - Message deliver mode. + * @param {string} newsgroups - Raw newsgroups header content. + * @returns {{newsgroups: string, newshost: string}} + */ + getNewsgroups(deliverMode, newsgroups) { + let nntpService = Cc["@mozilla.org/messenger/nntpservice;1"].getService( + Ci.nsINntpService + ); + let newsgroupsHeaderVal = {}; + let newshostHeaderVal = {}; + nntpService.generateNewsHeaderValsForPosting( + newsgroups, + newsgroupsHeaderVal, + newshostHeaderVal + ); + + // If we are here, we are NOT going to send this now. (i.e. it is a Draft, + // Send Later file, etc...). Because of that, we need to store what the user + // typed in on the original composition window for use later when rebuilding + // the headers + if ( + deliverMode == Ci.nsIMsgSend.nsMsgDeliverNow || + deliverMode == Ci.nsIMsgSend.nsMsgSendUnsent + ) { + // This is going to be saved for later, that means we should just store + // what the user typed into the "Newsgroup" line in the + // HEADER_X_MOZILLA_NEWSHOST header for later use by "Send Unsent + // Messages", "Drafts" or "Templates" + newshostHeaderVal.value = ""; + } + return { + newsgroups: newsgroupsHeaderVal.value, + newshost: newshostHeaderVal.value, + }; + }, + + /** + * Get the Content-Location header value. + * + * @param {string} baseUrl - The base url of an HTML attachment. + * @returns {string} + */ + getContentLocation(baseUrl) { + let lowerBaseUrl = baseUrl.toLowerCase(); + if ( + !baseUrl.includes(":") || + lowerBaseUrl.startsWith("news:") || + lowerBaseUrl.startsWith("snews:") || + lowerBaseUrl.startsWith("imap:") || + lowerBaseUrl.startsWith("file:") || + lowerBaseUrl.startsWith("mailbox:") + ) { + return ""; + } + let transformMap = { + " ": "%20", + "\t": "%09", + "\n": "%0A", + "\r": "%0D", + }; + let value = ""; + for (let char of baseUrl) { + value += transformMap[char] || char; + } + return value; + }, + + /** + * Given a string, convert it to 'qtext' (quoted text) for RFC822 header + * purposes. + */ + makeFilenameQtext(srcText, stripCRLFs) { + let size = srcText.length; + let ret = ""; + for (let i = 0; i < size; i++) { + let char = srcText.charAt(i); + if ( + char == "\\" || + char == '"' || + (!stripCRLFs && + char == "\r" && + (srcText[i + 1] != "\n" || + (srcText[i + 1] == "\n" && i + 2 < size && srcText[i + 2] != " "))) + ) { + ret += "\\"; + } + + if ( + stripCRLFs && + char == "\r" && + srcText[i + 1] == "\n" && + i + 2 < size && + srcText[i + 2] == " " + ) { + i += 3; + } else { + ret += char; + } + } + return ret; + }, + + /** + * Encode parameter value according to RFC 2047. + * + * @param {string} value - The parameter value. + * @returns {string} + */ + rfc2047EncodeParam(value) { + let converter = Cc["@mozilla.org/messenger/mimeconverter;1"].getService( + Ci.nsIMimeConverter + ); + + let encoded = converter.encodeMimePartIIStr_UTF8( + value, + false, + 0, + Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE + ); + + return this.makeFilenameQtext(encoded, false); + }, + + /** + * Encode parameter value according to RFC 2231. + * + * @param {string} paramName - The parameter name. + * @param {string} paramValue - The parameter value. + * @returns {string} + */ + rfc2231ParamFolding(paramName, paramValue) { + // this is to guarantee the folded line will never be greater + // than 78 = 75 + CRLFLWSP + const PR_MAX_FOLDING_LEN = 75; + + let needsEscape = false; + let encoder = new TextEncoder(); + let dupParamValue = jsmime.mimeutils.typedArrayToString( + encoder.encode(paramValue) + ); + + if (/[\x80-\xff]/.test(dupParamValue)) { + needsEscape = true; + dupParamValue = Services.io.escapeString( + dupParamValue, + Ci.nsINetUtil.ESCAPE_ALL + ); + } else { + dupParamValue = this.makeFilenameQtext(dupParamValue, true); + } + + let paramNameLen = paramName.length; + let paramValueLen = dupParamValue.length; + paramNameLen += 5; // *=__'__'___ or *[0]*=__'__'__ or *[1]*=___ or *[0]="___" + let foldedParam = ""; + + if (paramValueLen + paramNameLen + "UTF-8".length < PR_MAX_FOLDING_LEN) { + foldedParam = paramName; + if (needsEscape) { + foldedParam += "*=UTF-8''"; + } else { + foldedParam += '="'; + } + foldedParam += dupParamValue; + if (!needsEscape) { + foldedParam += '"'; + } + } else { + let curLineLen = 0; + let counter = 0; + let start = 0; + let end = null; + + while (paramValueLen > 0) { + curLineLen = 0; + if (counter == 0) { + foldedParam = paramName; + } else { + foldedParam += `;\r\n ${paramName}`; + } + foldedParam += `*${counter}`; + curLineLen += `*${counter}`.length; + if (needsEscape) { + foldedParam += "*="; + if (counter == 0) { + foldedParam += "UTF-8''"; + curLineLen += "UTF-8".length; + } + } else { + foldedParam += '="'; + } + counter++; + curLineLen += paramNameLen; + if (paramValueLen <= PR_MAX_FOLDING_LEN - curLineLen) { + end = start + paramValueLen; + } else { + end = start + (PR_MAX_FOLDING_LEN - curLineLen); + } + + if (end && needsEscape) { + // Check to see if we are in the middle of escaped char. + // We use ESCAPE_ALL, so every third character is a '%'. + if (end - 1 > start && dupParamValue[end - 1] == "%") { + end -= 1; + } else if (end - 2 > start && dupParamValue[end - 2] == "%") { + end -= 2; + } + // *end is now a '%'. + // Check if the following UTF-8 octet is a continuation. + while (end - 3 > start && "89AB".includes(dupParamValue[end + 1])) { + end -= 3; + } + } + foldedParam += dupParamValue.slice(start, end); + if (!needsEscape) { + foldedParam += '"'; + } + paramValueLen -= end - start; + start = end; + } + } + + return foldedParam; + }, + + /** + * Get the target message folder to copy to. + * + * @param {nsIMsgIdentity} userIdentity - The user identity. + * @param {nsMsgDeliverMode} deliverMode - The deliver mode. + * @returns {string} + */ + getMsgFolderURIFromPrefs(userIdentity, deliverMode) { + if ( + deliverMode == Ci.nsIMsgSend.nsMsgQueueForLater || + deliverMode == Ci.nsIMsgSend.nsMsgDeliverBackground + ) { + let uri = Services.prefs.getCharPref("mail.default_sendlater_uri"); + // check if uri is unescaped, and if so, escape it and reset the pef. + if (!uri) { + return "anyfolder://"; + } else if (uri.includes(" ")) { + uri.replaceAll(" ", "%20"); + Services.prefs.setCharPref("mail.default_sendlater_uri", uri); + } + return uri; + } else if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft) { + return userIdentity.draftFolder; + } else if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate) { + return userIdentity.stationeryFolder; + } + if (userIdentity.doFcc) { + return userIdentity.fccFolder; + } + return ""; + }, + + /** + * Get the error string name of an exit code. The name will corresponds to an + * entry in composeMsgs.properties. + * + * @param {nsresult} exitCode - Exit code of sending mail process. + * @returns {string} + */ + getErrorStringName(exitCode) { + let codeNameMap = { + [this.NS_MSG_UNABLE_TO_OPEN_FILE]: "unableToOpenFile", + [this.NS_MSG_UNABLE_TO_OPEN_TMP_FILE]: "unableToOpenTmpFile", + [this.NS_MSG_UNABLE_TO_SAVE_TEMPLATE]: "unableToSaveTemplate", + [this.NS_MSG_UNABLE_TO_SAVE_DRAFT]: "unableToSaveDraft", + [this.NS_MSG_COULDNT_OPEN_FCC_FOLDER]: "couldntOpenFccFolder", + [this.NS_MSG_NO_SENDER]: "noSender", + [this.NS_MSG_NO_RECIPIENTS]: "noRecipients", + [this.NS_MSG_ERROR_WRITING_FILE]: "errorWritingFile", + [this.NS_ERROR_SENDING_FROM_COMMAND]: "errorSendingFromCommand", + [this.NS_ERROR_SENDING_DATA_COMMAND]: "errorSendingDataCommand", + [this.NS_ERROR_SENDING_MESSAGE]: "errorSendingMessage", + [this.NS_ERROR_POST_FAILED]: "postFailed", + [this.NS_ERROR_SMTP_SERVER_ERROR]: "smtpServerError", + [this.NS_MSG_UNABLE_TO_SEND_LATER]: "unableToSendLater", + [this.NS_ERROR_COMMUNICATIONS_ERROR]: "communicationsError", + [this.NS_ERROR_BUT_DONT_SHOW_ALERT]: "dontShowAlert", + [this.NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS]: + "couldNotGetUsersMailAddress2", + [this.NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY]: + "couldNotGetSendersIdentity", + [this.NS_ERROR_MIME_MPART_ATTACHMENT_ERROR]: "mimeMpartAttachmentError", + [this.NS_ERROR_NNTP_NO_CROSS_POSTING]: "nntpNoCrossPosting", + [this.NS_MSG_ERROR_READING_FILE]: "errorReadingFile", + [this.NS_MSG_ERROR_ATTACHING_FILE]: "errorAttachingFile", + [this.NS_ERROR_SMTP_GREETING]: "incorrectSmtpGreeting", + [this.NS_ERROR_SENDING_RCPT_COMMAND]: "errorSendingRcptCommand", + [this.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS]: "startTlsFailed", + [this.NS_ERROR_SMTP_PASSWORD_UNDEFINED]: "smtpPasswordUndefined", + [this.NS_ERROR_SMTP_SEND_NOT_ALLOWED]: "smtpSendNotAllowed", + [this.NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED]: "smtpTooManyRecipients", + [this.NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2]: "smtpPermSizeExceeded2", + [this.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER]: + "smtpSendFailedUnknownServer", + [this.NS_ERROR_SMTP_SEND_FAILED_REFUSED]: "smtpSendRequestRefused", + [this.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED]: "smtpSendInterrupted", + [this.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT]: "smtpSendTimeout", + [this.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON]: + "smtpSendFailedUnknownReason", + [this.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL]: + "smtpHintAuthEncryptToPlainNoSsl", + [this.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL]: + "smtpHintAuthEncryptToPlainSsl", + [this.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT]: + "smtpHintAuthPlainToEncrypt", + [this.NS_ERROR_SMTP_AUTH_FAILURE]: "smtpAuthFailure", + [this.NS_ERROR_SMTP_AUTH_GSSAPI]: "smtpAuthGssapi", + [this.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED]: "smtpAuthMechNotSupported", + [this.NS_ERROR_ILLEGAL_LOCALPART]: "errorIllegalLocalPart2", + [this.NS_ERROR_CLIENTID]: "smtpClientid", + [this.NS_ERROR_CLIENTID_PERMISSION]: "smtpClientidPermission", + }; + return codeNameMap[exitCode] || "sendFailed"; + }, + + /** + * Format the error message that will be shown to the user. + * + * @param {nsIMsgIdentity} userIdentity - User identity. + * @param {nsIStringBundle} composeBundle - Localized string bundle. + * @param {string} errorName - The error name derived from an exit code. + * @returns {string} + */ + formatStringWithSMTPHostName(userIdentity, composeBundle, errorName) { + let smtpServer = MailServices.smtp.getServerByIdentity(userIdentity); + let smtpHostname = smtpServer.hostname; + return composeBundle.formatStringFromName(errorName, [smtpHostname]); + }, + + /** + * Generate random alphanumeric string. + * + * @param {number} size - The length of generated string. + * @returns {string} + */ + randomString(size) { + let length = Math.round((size * 3) / 4); + return btoa( + String.fromCharCode( + ...[...Array(length)].map(() => Math.floor(Math.random() * 256)) + ) + ) + .slice(0, size) + .replaceAll(/[+/=]/g, "0"); + }, + + /** + * Generate a content id to be used by embedded images. + * + * @param {nsIMsgIdentity} userIdentity - User identity. + * @param {number} partNum - The number of embedded MimePart. + * @returns {string} + */ + makeContentId(userIdentity, partNum) { + let domain = userIdentity.email.split("@")[1]; + return `part${partNum}.${this.randomString(8)}.${this.randomString( + 8 + )}@${domain}`; + }, + + /** + * Pick a file name from the file URL. + * + * @param {string} url - The file URL. + * @returns {string} + */ + pickFileNameFromUrl(url) { + if (/^(news|snews|imap|mailbox):/i.test(url)) { + // No sensible file name in it, + return ""; + } + if (/^data:/i.test(url)) { + let matches = /filename=(.*);/.exec(url); + if (matches && matches[1]) { + return decodeURIComponent(matches[1]); + } + let mimeType = url.slice(5, url.indexOf(";")); + let extname = ""; + try { + extname = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getPrimaryExtension(mimeType, null); + if (!extname) { + return ""; + } + } catch (e) { + return ""; + } + return `${this.randomString(16)}.${extname}`; + } + // Take the part after the last / or \. + let lastSlash = url.lastIndexOf("\\"); + if (lastSlash == -1) { + lastSlash = url.lastIndexOf("/"); + } + // Strip any search or anchor. + return url + .slice(lastSlash + 1) + .split("?")[0] + .split("#")[0]; + }, +}; diff --git a/comm/mailnews/compose/src/MimePart.jsm b/comm/mailnews/compose/src/MimePart.jsm new file mode 100644 index 0000000000..a22c0527af --- /dev/null +++ b/comm/mailnews/compose/src/MimePart.jsm @@ -0,0 +1,378 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MimePart", "MimeMultiPart"]; + +let { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm"); +let { MimeEncoder } = ChromeUtils.import("resource:///modules/MimeEncoder.jsm"); +let { MsgUtils } = ChromeUtils.import( + "resource:///modules/MimeMessageUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailStringUtils } = ChromeUtils.import( + "resource:///modules/MailStringUtils.jsm" +); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +/** + * A class to represent a RFC2045 message. MimePart can be nested, each MimePart + * can contain a list of MimePart. HTML and plain text are parts as well. Use + * class MimeMultiPart for multipart/*, that's why this class doesn't expose an + * addPart method + */ +class MimePart { + /** + * @param {string} contentType - Content type of the part, e.g. text/plain. + * @param {boolean} forceMsgEncoding - A flag used to determine Content-Transfer-Encoding. + * @param {boolean} isMainBody - The part is main part or an attachment part. + */ + constructor(contentType = "", forceMsgEncoding = false, isMainBody = false) { + this._charset = "UTF-8"; + this._contentType = contentType; + this._forceMsgEncoding = forceMsgEncoding; + this._isMainBody = isMainBody; + + this._headers = new Map(); + // 8-bit string to avoid converting back and forth. + this._bodyText = ""; + this._bodyAttachment = null; + this._contentDisposition = null; + this._contentId = null; + this._separator = ""; + this._parts = []; + } + + /** + * @type {BinaryString} text - The string to use as body. + */ + set bodyText(text) { + this._bodyText = text.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n"); + } + + /** + * @type {MimePart[]} - The child parts. + */ + get parts() { + return this._parts; + } + + /** + * @type {MimePart[]} parts - The child parts. + */ + set parts(parts) { + this._parts = parts; + } + + /** + * @type {string} - The separator string. + */ + get separator() { + return this._separator; + } + + /** + * Set a header. + * + * @param {string} name - The header name, e.g. "content-type". + * @param {string} content - The header content, e.g. "text/plain". + */ + setHeader(name, content) { + if (!content) { + return; + } + // There is no Content-Type encoder in jsmime yet. If content is not string, + // assume it's already a structured header. + if (name == "content-type" || typeof content != "string") { + // _headers will be passed to jsmime, which requires header content to be + // an array. + this._headers.set(name, [content]); + return; + } + try { + this._headers.set(name, [ + jsmime.headerparser.parseStructuredHeader(name, content), + ]); + } catch (e) { + this._headers.set(name, [content.trim()]); + } + } + + /** + * Delete a header. + * + * @param {string} name - The header name to delete, e.g. "content-type". + */ + deleteHeader(name) { + this._headers.delete(name); + } + + /** + * Set headers by an iterable. + * + * @param {Iterable.<string, string>} entries - The header entries. + */ + setHeaders(entries) { + for (let [name, content] of entries) { + this.setHeader(name, content); + } + } + + /** + * Set an attachment as body, with optional contentDisposition and contentId. + * + * @param {nsIMsgAttachment} attachment - The attachment to use as body. + * @param {string} [contentDisposition=attachment] - "attachment" or "inline". + * @param {string} [contentId] - The url of an embedded object is cid:contentId. + */ + setBodyAttachment( + attachment, + contentDisposition = "attachment", + contentId = null + ) { + this._bodyAttachment = attachment; + this._contentDisposition = contentDisposition; + this._contentId = contentId; + } + + /** + * Add a child part. + * + * @param {MimePart} part - A MimePart. + */ + addPart(part) { + this._parts.push(part); + } + + /** + * Add child parts. + * + * @param {MimePart[]} parts - An array of MimePart. + */ + addParts(parts) { + this._parts.push(...parts); + } + + /** + * Pick an encoding according to _bodyText or _bodyAttachment content. Set + * content-transfer-encoding header, then return the encoded value. + * + * @returns {BinaryString} + */ + async getEncodedBodyString() { + let bodyString = this._bodyText; + // If this is an attachment part, use the attachment content as bodyString. + if (this._bodyAttachment) { + try { + bodyString = await this._fetchAttachment(); + } catch (e) { + MsgUtils.sendLogger.error( + `Failed to fetch attachment; name=${this._bodyAttachment.name}, url=${this._bodyAttachment.url}, error=${e}` + ); + throw Components.Exception( + "Failed to fetch attachment", + MsgUtils.NS_MSG_ERROR_ATTACHING_FILE, + e.stack, + this._bodyAttachment + ); + } + } + if (bodyString) { + let encoder = new MimeEncoder( + this._charset, + this._contentType, + this._forceMsgEncoding, + this._isMainBody, + bodyString + ); + encoder.pickEncoding(); + this.setHeader("content-transfer-encoding", encoder.encoding); + bodyString = encoder.encode(); + } else if (this._isMainBody) { + this.setHeader("content-transfer-encoding", "7bit"); + } + return bodyString; + } + + /** + * Use jsmime to convert _headers to string. + * + * @returns {string} + */ + getHeaderString() { + return jsmime.headeremitter.emitStructuredHeaders(this._headers, { + useASCII: true, + sanitizeDate: Services.prefs.getBoolPref( + "mail.sanitize_date_header", + false + ), + }); + } + + /** + * Fetch the attached message file to get its content. + * + * @returns {string} + */ + async _fetchMsgAttachment() { + let msgService = MailServices.messageServiceFromURI( + this._bodyAttachment.url + ); + return new Promise((resolve, reject) => { + let streamListener = { + _data: "", + _stream: null, + onDataAvailable(request, inputStream, offset, count) { + if (!this._stream) { + this._stream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].createInstance(Ci.nsIScriptableInputStream); + this._stream.init(inputStream); + } + this._data += this._stream.read(count); + }, + onStartRequest() {}, + onStopRequest(request, status) { + if (Components.isSuccessCode(status)) { + resolve(this._data); + } else { + reject(`Fetch message attachment failed with status=${status}`); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + }; + + msgService.streamMessage( + this._bodyAttachment.url, + streamListener, + null, // msgWindow + null, // urlListener + false, // convertData + "" // additionalHeader + ); + }); + } + + /** + * Fetch the attachment file to get its content type and content. + * + * Previously, we used the Fetch API to download all attachments, but Fetch + * doesn't support url with embedded credentials (imap://name@server). As a + * result, it's unreliable when having two mail accounts on the same IMAP + * server. + * + * @returns {string} + */ + async _fetchAttachment() { + let url = this._bodyAttachment.url; + MsgUtils.sendLogger.debug(`Fetching ${url}`); + + let content = ""; + if (/^[^:]+-message:/i.test(url)) { + content = await this._fetchMsgAttachment(); + if (!content) { + // Message content is empty usually means it's (re)moved. + throw new Error("Message is gone"); + } + this._contentType = "message/rfc822"; + } else { + let channel = Services.io.newChannelFromURI( + Services.io.newURI(url), + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + content = await new Promise((resolve, reject) => + NetUtil.asyncFetch(channel, (stream, status, request) => { + if (!Components.isSuccessCode(status)) { + reject(`asyncFetch failed with status=${status}`); + return; + } + let data = ""; + try { + data = NetUtil.readInputStreamToString(stream, stream.available()); + } catch (e) { + // stream.available() throws if the file is empty. + } + resolve(data); + }) + ); + this._contentType = + this._bodyAttachment.contentType || channel.contentType; + } + + let parmFolding = Services.prefs.getIntPref( + "mail.strictly_mime.parm_folding", + 2 + ); + // File name can contain non-ASCII chars, encode according to RFC 2231. + let encodedName, encodedFileName; + if (this._bodyAttachment.name) { + encodedName = MsgUtils.rfc2047EncodeParam(this._bodyAttachment.name); + encodedFileName = MsgUtils.rfc2231ParamFolding( + "filename", + this._bodyAttachment.name + ); + } + this._charset = this._contentType.startsWith("text/") + ? MailStringUtils.detectCharset(content) + : ""; + + let contentTypeParams = ""; + if (this._charset) { + contentTypeParams += `; charset=${this._charset}`; + } + if (encodedName && parmFolding != 2) { + contentTypeParams += `; name="${encodedName}"`; + } + this.setHeader("content-type", `${this._contentType}${contentTypeParams}`); + if (encodedFileName) { + this.setHeader( + "content-disposition", + `${this._contentDisposition}; ${encodedFileName}` + ); + } + if (this._contentId) { + this.setHeader("content-id", `<${this._contentId}>`); + } + if (this._contentType == "text/html") { + let contentLocation = MsgUtils.getContentLocation( + this._bodyAttachment.url + ); + this.setHeader("content-location", contentLocation); + } else if (this._contentType == "application/pgp-keys") { + this.setHeader("content-description", "OpenPGP public key"); + } + + return content; + } +} + +/** + * A class to represent a multipart/* part inside a RFC2045 message. + */ +class MimeMultiPart extends MimePart { + /** + * @param {string} subtype - The multipart subtype, e.g. "alternative" or "mixed". + */ + constructor(subtype) { + super(); + this.subtype = subtype; + this._separator = this._makePartSeparator(); + this.setHeader( + "content-type", + `multipart/${subtype}; boundary="${this._separator}"` + ); + } + + /** + * Use 12 hyphen characters and 24 random base64 characters as separator. + */ + _makePartSeparator() { + return "------------" + MsgUtils.randomString(24); + } +} diff --git a/comm/mailnews/compose/src/SMTPProtocolHandler.jsm b/comm/mailnews/compose/src/SMTPProtocolHandler.jsm new file mode 100644 index 0000000000..d139e15784 --- /dev/null +++ b/comm/mailnews/compose/src/SMTPProtocolHandler.jsm @@ -0,0 +1,39 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["SMTPProtocolHandler", "SMTPSProtocolHandler"]; + +/** + * @implements {nsIProtocolHandler} + */ +class SMTPProtocolHandler { + QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]); + + scheme = "smtp"; + + newChannel(aURI, aLoadInfo) { + throw Components.Exception( + `${this.constructor.name}.newChannel not implemented`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + allowPort(port, scheme) { + return port == Ci.nsISmtpUrl.DEFAULT_SMTP_PORT; + } +} +SMTPProtocolHandler.prototype.classID = Components.ID( + "{b14c2b67-8680-4c11-8d63-9403c7d4f757}" +); + +class SMTPSProtocolHandler extends SMTPProtocolHandler { + scheme = "smtps"; + + allowPort(port, scheme) { + return port == Ci.nsISmtpUrl.DEFAULT_SMTPS_PORT; + } +} +SMTPSProtocolHandler.prototype.classID = Components.ID( + "{057d0997-9e3a-411e-b4ee-2602f53fe05f}" +); diff --git a/comm/mailnews/compose/src/SmtpClient.jsm b/comm/mailnews/compose/src/SmtpClient.jsm new file mode 100644 index 0000000000..2eb13985e3 --- /dev/null +++ b/comm/mailnews/compose/src/SmtpClient.jsm @@ -0,0 +1,1344 @@ +/* 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 http://mozilla.org/MPL/2.0/. + * + * Based on https://github.com/emailjs/emailjs-smtp-client + * + * Copyright (c) 2013 Andris Reinman + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +const EXPORTED_SYMBOLS = ["SmtpClient"]; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +var { MailStringUtils } = ChromeUtils.import( + "resource:///modules/MailStringUtils.jsm" +); +var { SmtpAuthenticator } = ChromeUtils.import( + "resource:///modules/MailAuthenticator.jsm" +); +var { MsgUtils } = ChromeUtils.import( + "resource:///modules/MimeMessageUtils.jsm" +); + +class SmtpClient { + /** + * The number of RCPT TO commands sent on the connection by this client. + * This can count-up over multiple messages. + */ + rcptCount = 0; + + /** + * Set true only when doing a retry. + */ + isRetry = false; + + /** + * Creates a connection object to a SMTP server and allows to send mail through it. + * Call `connect` method to inititate the actual connection, the constructor only + * defines the properties but does not actually connect. + * + * @class + * + * @param {nsISmtpServer} server - The associated nsISmtpServer instance. + */ + constructor(server) { + this.options = { + alwaysSTARTTLS: + server.socketType == Ci.nsMsgSocketType.trySTARTTLS || + server.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS, + requireTLS: server.socketType == Ci.nsMsgSocketType.SSL, + }; + + this.socket = false; // Downstream TCP socket to the SMTP server, created with TCPSocket + this.waitDrain = false; // Keeps track if the downstream socket is currently full and a drain event should be waited for or not + + // Private properties + + this._server = server; + this._authenticator = new SmtpAuthenticator(server); + this._authenticating = false; + // A list of auth methods detected from the EHLO response. + this._supportedAuthMethods = []; + // A list of auth methods that worth a try. + this._possibleAuthMethods = []; + // Auth method set by user preference. + this._preferredAuthMethods = + { + [Ci.nsMsgAuthMethod.passwordCleartext]: ["PLAIN", "LOGIN"], + [Ci.nsMsgAuthMethod.passwordEncrypted]: ["CRAM-MD5"], + [Ci.nsMsgAuthMethod.GSSAPI]: ["GSSAPI"], + [Ci.nsMsgAuthMethod.NTLM]: ["NTLM"], + [Ci.nsMsgAuthMethod.OAuth2]: ["XOAUTH2"], + [Ci.nsMsgAuthMethod.secure]: ["CRAM-MD5", "XOAUTH2"], + }[server.authMethod] || []; + // The next auth method to try if the current failed. + this._nextAuthMethod = null; + + // A list of capabilities detected from the EHLO response. + this._capabilities = []; + + this._dataMode = false; // If true, accepts data from the upstream to be passed directly to the downstream socket. Used after the DATA command + this._lastDataBytes = ""; // Keep track of the last bytes to see how the terminating dot should be placed + this._envelope = null; // Envelope object for tracking who is sending mail to whom + this._currentAction = null; // Stores the function that should be run after a response has been received from the server + + this._parseBlock = { data: [], statusCode: null }; + this._parseRemainder = ""; // If the complete line is not received yet, contains the beginning of it + + this.logger = MsgUtils.smtpLogger; + + // Event placeholders + this.onerror = (e, failedSecInfo) => {}; // Will be run when an error occurs. The `onclose` event will fire subsequently. + this.ondrain = () => {}; // More data can be buffered in the socket. + this.onclose = () => {}; // The connection to the server has been closed + this.onidle = () => {}; // The connection is established and idle, you can send mail now + this.onready = failedRecipients => {}; // Waiting for mail body, lists addresses that were not accepted as recipients + this.ondone = success => {}; // The mail has been sent. Wait for `onidle` next. Indicates if the message was queued by the server. + // Callback when this client is ready to be reused. + this.onFree = () => {}; + } + + /** + * Initiate a connection to the server + */ + connect() { + if (this.socket?.readyState == "open") { + this.logger.debug("Reusing a connection"); + this.onidle(); + } else { + let hostname = this._server.hostname.toLowerCase(); + let port = this._server.port || (this.options.requireTLS ? 465 : 587); + this.logger.debug(`Connecting to smtp://${hostname}:${port}`); + this._secureTransport = this.options.requireTLS; + this.socket = new TCPSocket(hostname, port, { + binaryType: "arraybuffer", + useSecureTransport: this._secureTransport, + }); + + this.socket.onerror = this._onError; + this.socket.onopen = this._onOpen; + } + this._freed = false; + } + + /** + * Sends QUIT + */ + quit() { + this._authenticating = false; + this._freed = true; + this._sendCommand("QUIT"); + this._currentAction = this.close; + } + + /** + * Closes the connection to the server + * + * @param {boolean} [immediately] - Close the socket without waiting for + * unsent data. + */ + close(immediately) { + if (this.socket && this.socket.readyState === "open") { + if (immediately) { + this.logger.debug( + `Closing connection to ${this._server.hostname} immediately!` + ); + this.socket.closeImmediately(); + } else { + this.logger.debug(`Closing connection to ${this._server.hostname}...`); + this.socket.close(); + } + } else { + this.logger.debug(`Connection to ${this._server.hostname} closed`); + this._free(); + } + } + + // Mail related methods + + /** + * Initiates a new message by submitting envelope data, starting with + * `MAIL FROM:` command. Use after `onidle` event + * + * @param {object} envelope - The envelope object. + * @param {string} envelope.from - The from address. + * @param {string[]} envelope.to - The to addresses. + * @param {number} envelope.size - The file size. + * @param {boolean} envelope.requestDSN - Whether to request Delivery Status Notifications. + * @param {boolean} envelope.messageId - The message id. + */ + useEnvelope(envelope) { + this._envelope = envelope || {}; + this._envelope.from = [].concat( + this._envelope.from || "anonymous@" + this._getHelloArgument() + )[0]; + + if (!this._capabilities.includes("SMTPUTF8")) { + // If server doesn't support SMTPUTF8, check if addresses contain invalid + // characters. + + let recipients = this._envelope.to; + this._envelope.to = []; + + for (let recipient of recipients) { + let lastAt = null; + let firstInvalid = null; + for (let i = 0; i < recipient.length; i++) { + let ch = recipient[i]; + if (ch == "@") { + lastAt = i; + } else if ((ch < " " || ch > "~") && ch != "\t") { + firstInvalid = i; + break; + } + } + if (!recipient || firstInvalid != null) { + if (!lastAt) { + // Invalid char found in the localpart, throw error until we implement RFC 6532. + this._onNsError(MsgUtils.NS_ERROR_ILLEGAL_LOCALPART, recipient); + return; + } + // Invalid char found in the domainpart, convert it to ACE. + let idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + let domain = idnService.convertUTF8toACE(recipient.slice(lastAt + 1)); + recipient = `${recipient.slice(0, lastAt)}@${domain}`; + } + this._envelope.to.push(recipient); + } + } + + // clone the recipients array for latter manipulation + this._envelope.rcptQueue = [...new Set(this._envelope.to)]; + this._envelope.rcptFailed = []; + this._envelope.responseQueue = []; + + if (!this._envelope.rcptQueue.length) { + this._onNsError(MsgUtils.NS_MSG_NO_RECIPIENTS); + return; + } + + this._currentAction = this._actionMAIL; + let cmd = `MAIL FROM:<${this._envelope.from}>`; + if ( + this._capabilities.includes("8BITMIME") && + !Services.prefs.getBoolPref("mail.strictly_mime", false) + ) { + cmd += " BODY=8BITMIME"; + } + if (this._capabilities.includes("SMTPUTF8")) { + // Should not send SMTPUTF8 if all ascii, see RFC6531. + // eslint-disable-next-line no-control-regex + let ascii = /^[\x00-\x7F]+$/; + if ([envelope.from, ...envelope.to].some(x => !ascii.test(x))) { + cmd += " SMTPUTF8"; + } + } + if (this._capabilities.includes("SIZE")) { + cmd += ` SIZE=${this._envelope.size}`; + } + if (this._capabilities.includes("DSN") && this._envelope.requestDSN) { + let ret = Services.prefs.getBoolPref("mail.dsn.ret_full_on") + ? "FULL" + : "HDRS"; + cmd += ` RET=${ret} ENVID=${envelope.messageId}`; + } + this._sendCommand(cmd); + } + + /** + * Send ASCII data to the server. Works only in data mode (after `onready` event), ignored + * otherwise + * + * @param {string} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server + * @returns {boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more + */ + send(chunk) { + // works only in data mode + if (!this._dataMode) { + // this line should never be reached but if it does, + // act like everything's normal. + return true; + } + + // TODO: if the chunk is an arraybuffer, use a separate function to send the data + return this._sendString(chunk); + } + + /** + * Indicates that a data stream for the socket is ended. Works only in data + * mode (after `onready` event), ignored otherwise. Use it when you are done + * with sending the mail. This method does not close the socket. Once the mail + * has been queued by the server, `ondone` and `onidle` are emitted. + * + * @param {Buffer} [chunk] Chunk of data to be sent to the server + */ + end(chunk) { + // works only in data mode + if (!this._dataMode) { + // this line should never be reached but if it does, + // act like everything's normal. + return true; + } + + if (chunk && chunk.length) { + this.send(chunk); + } + + // redirect output from the server to _actionStream + this._currentAction = this._actionStream; + + // indicate that the stream has ended by sending a single dot on its own line + // if the client already closed the data with \r\n no need to do it again + if (this._lastDataBytes === "\r\n") { + this.waitDrain = this._send(new Uint8Array([0x2e, 0x0d, 0x0a]).buffer); // .\r\n + } else if (this._lastDataBytes.substr(-1) === "\r") { + this.waitDrain = this._send( + new Uint8Array([0x0a, 0x2e, 0x0d, 0x0a]).buffer + ); // \n.\r\n + } else { + this.waitDrain = this._send( + new Uint8Array([0x0d, 0x0a, 0x2e, 0x0d, 0x0a]).buffer + ); // \r\n.\r\n + } + + // End data mode. + this._dataMode = false; + + return this.waitDrain; + } + + // PRIVATE METHODS + + /** + * Queue some data from the server for parsing. + * + * @param {string} chunk Chunk of data received from the server + */ + _parse(chunk) { + // Lines should always end with <CR><LF> but you never know, might be only <LF> as well + var lines = (this._parseRemainder + (chunk || "")).split(/\r?\n/); + this._parseRemainder = lines.pop(); // not sure if the line has completely arrived yet + + for (let i = 0, len = lines.length; i < len; i++) { + if (!lines[i].trim()) { + // nothing to check, empty line + continue; + } + + // possible input strings for the regex: + // 250-MULTILINE REPLY + // 250 LAST LINE OF REPLY + // 250 1.2.3 MESSAGE + + const match = lines[i].match( + /^(\d{3})([- ])(?:(\d+\.\d+\.\d+)(?: ))?(.*)/ + ); + + if (match) { + this._parseBlock.data.push(match[4]); + + if (match[2] === "-") { + // this is a multiline reply + this._parseBlock.statusCode = + this._parseBlock.statusCode || Number(match[1]); + } else { + const statusCode = Number(match[1]) || 0; + const response = { + statusCode, + data: this._parseBlock.data.join("\n"), + // Success means can move to the next step. Though 3xx is not + // failure, we don't consider it success here. + success: statusCode >= 200 && statusCode < 300, + }; + + this._onCommand(response); + this._parseBlock = { + data: [], + statusCode: null, + }; + } + } else { + this._onCommand({ + success: false, + statusCode: this._parseBlock.statusCode || null, + data: [lines[i]].join("\n"), + }); + this._parseBlock = { + data: [], + statusCode: null, + }; + } + } + } + + // EVENT HANDLERS FOR THE SOCKET + + /** + * Connection listener that is run when the connection to the server is opened. + * Sets up different event handlers for the opened socket + */ + _onOpen = () => { + this.logger.debug("Connected"); + + this.socket.ondata = this._onData; + this.socket.onclose = this._onClose; + this.socket.ondrain = this._onDrain; + + this._currentAction = this._actionGreeting; + this.socket.transport.setTimeout( + Ci.nsISocketTransport.TIMEOUT_READ_WRITE, + Services.prefs.getIntPref("mailnews.tcptimeout") + ); + }; + + /** + * Data listener for chunks of data emitted by the server + * + * @param {Event} evt - Event object. See `evt.data` for the chunk received + */ + _onData = async evt => { + let stringPayload = new TextDecoder("UTF-8").decode( + new Uint8Array(evt.data) + ); + // "S: " to denote that this is data from the Server. + this.logger.debug(`S: ${stringPayload}`); + + // Prevent blocking the main thread, otherwise onclose/onerror may not be + // called in time. test_smtpPasswordFailure3 is such a case, the server + // rejects AUTH PLAIN then closes the connection, the client then sends AUTH + // LOGIN. This line guarantees onclose is called before sending AUTH LOGIN. + await new Promise(resolve => setTimeout(resolve)); + this._parse(stringPayload); + }; + + /** + * More data can be buffered in the socket, `waitDrain` is reset to false + */ + _onDrain = () => { + this.waitDrain = false; + this.ondrain(); + }; + + /** + * Error handler. Emits an nsresult value. + * + * @param {Error|TCPSocketErrorEvent} event - An Error or TCPSocketErrorEvent object. + */ + _onError = async event => { + this.logger.error(`${event.name}: a ${event.message} error occurred`); + if (this._freed) { + // Ignore socket errors if already freed. + return; + } + + this._free(); + this.quit(); + + let nsError = Cr.NS_ERROR_FAILURE; + let secInfo = null; + if (TCPSocketErrorEvent.isInstance(event)) { + nsError = event.errorCode; + secInfo = + await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo(); + if (secInfo) { + this.logger.error(`SecurityError info: ${secInfo.errorCodeString}`); + if (secInfo.failedCertChain.length) { + let chain = secInfo.failedCertChain.map(c => { + return c.commonName + "; serial# " + c.serialNumber; + }); + this.logger.error(`SecurityError cert chain: ${chain.join(" <- ")}`); + } + this._server.closeCachedConnections(); + } + } + + // Use nsresult to integrate with other parts of sending process, e.g. + // MessageSend.jsm will show an error message depending on the nsresult. + this.onerror(nsError, "", secInfo); + }; + + /** + * Error handler. Emits an nsresult value. + * + * @param {nsresult} nsError - A nsresult. + * @param {string} errorParam - Param to form the error message. + * @param {string} [extra] - Some messages take two arguments to format. + * @param {number} [statusCode] - Only needed when checking need to retry. + */ + _onNsError(nsError, errorParam, extra, statusCode) { + // First check if handling an error response that might need a retry. + if ([this._actionMAIL, this._actionRCPT].includes(this._currentAction)) { + if (statusCode >= 400 && statusCode < 500) { + // Possibly too many recipients, too many messages, to much data + // or too much time has elapsed on this connection. + if (!this.isRetry) { + // Now seeing error 4xx meaning that the current message can't be + // accepted. We close the connection and try again to send on a new + // connection using this same client instance. If the retry also + // fails on the new connection, we give up and report the error. + this.logger.debug("Retry send on new connection."); + this.quit(); + this.isRetry = true; // flag that we will retry on new connection + this.close(true); + this.connect(); + return; // return without reporting the error yet + } + } + } + + let errorName = MsgUtils.getErrorStringName(nsError); + let errorMessage = ""; + if ( + [ + MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, + MsgUtils.NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED, + MsgUtils.NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2, + MsgUtils.NS_ERROR_SENDING_FROM_COMMAND, + MsgUtils.NS_ERROR_SENDING_RCPT_COMMAND, + MsgUtils.NS_ERROR_SENDING_DATA_COMMAND, + MsgUtils.NS_ERROR_SENDING_MESSAGE, + MsgUtils.NS_ERROR_ILLEGAL_LOCALPART, + ].includes(nsError) + ) { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + if (nsError == MsgUtils.NS_ERROR_ILLEGAL_LOCALPART) { + errorMessage = bundle + .GetStringFromName(errorName) + .replace("%s", errorParam); + } else { + errorMessage = bundle.formatStringFromName(errorName, [ + errorParam, + extra, + ]); + } + } + this.onerror(nsError, errorMessage); + this.close(); + } + + /** + * Indicates that the socket has been closed + */ + _onClose = () => { + this.logger.debug("Socket closed."); + this._free(); + this.rcptCount = 0; + if (this._authenticating) { + // In some cases, socket is closed for invalid username/password. + this._onAuthFailed({ data: "Socket closed." }); + } + }; + + /** + * This is not a socket data handler but the handler for data emitted by the parser, + * so this data is safe to use as it is always complete (server might send partial chunks) + * + * @param {object} command - Parsed data. + */ + _onCommand(command) { + if (command.statusCode < 200 || command.statusCode >= 400) { + // @see https://datatracker.ietf.org/doc/html/rfc5321#section-3.8 + // 421: SMTP service shutting down and closing transmission channel. + // When that happens during idle, just close the connection. + if ( + command.statusCode == 421 && + this._currentAction == this._actionIdle + ) { + this.close(true); + return; + } + + this.logger.error( + `Command failed: ${command.statusCode} ${command.data}; currentAction=${this._currentAction?.name}` + ); + } + if (typeof this._currentAction === "function") { + this._currentAction(command); + } + } + + /** + * This client has finished the current process and ready to be reused. + */ + _free() { + if (!this._freed) { + this._freed = true; + this.onFree(); + } + } + + /** + * Sends a string to the socket. + * + * @param {string} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server + * @returns {boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more + */ + _sendString(chunk) { + // escape dots + if (!this.options.disableEscaping) { + chunk = chunk.replace(/\n\./g, "\n.."); + if ( + (this._lastDataBytes.substr(-1) === "\n" || !this._lastDataBytes) && + chunk.charAt(0) === "." + ) { + chunk = "." + chunk; + } + } + + // Keeping eye on the last bytes sent, to see if there is a <CR><LF> sequence + // at the end which is needed to end the data stream + if (chunk.length > 2) { + this._lastDataBytes = chunk.substr(-2); + } else if (chunk.length === 1) { + this._lastDataBytes = this._lastDataBytes.substr(-1) + chunk; + } + + this.logger.debug("Sending " + chunk.length + " bytes of payload"); + + // pass the chunk to the socket + this.waitDrain = this._send( + MailStringUtils.byteStringToUint8Array(chunk).buffer + ); + return this.waitDrain; + } + + /** + * Send a string command to the server, also append CRLF if needed. + * + * @param {string} str - String to be sent to the server. + * @param {boolean} [suppressLogging=false] - If true and not in dev mode, + * do not log the str. For non-release builds output won't be suppressed, + * so that debugging auth problems is easier. + */ + _sendCommand(str, suppressLogging = false) { + if (this.socket.readyState !== "open") { + if (str != "QUIT") { + this.logger.warn( + `Failed to send "${str}" because socket state is ${this.socket.readyState}` + ); + } + return; + } + // "C: " is used to denote that this is data from the Client. + if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") { + this.logger.debug( + "C: Logging suppressed (it probably contained auth information)" + ); + } else { + this.logger.debug(`C: ${str}`); + } + this.waitDrain = this._send( + new TextEncoder().encode(str + (str.substr(-2) !== "\r\n" ? "\r\n" : "")) + .buffer + ); + } + + _send(buffer) { + return this.socket.send(buffer); + } + + /** + * Intitiate authentication sequence if needed + * + * @param {boolean} forceNewPassword - Discard cached password. + */ + async _authenticateUser(forceNewPassword) { + if ( + this._preferredAuthMethods.length == 0 || + this._supportedAuthMethods.length == 0 + ) { + // no need to authenticate, at least no data given + this._currentAction = this._actionIdle; + this.onidle(); // ready to take orders + return; + } + + if (!this._nextAuthMethod) { + this._onAuthFailed({ data: "No available auth method." }); + return; + } + + this._authenticating = true; + + this._currentAuthMethod = this._nextAuthMethod; + this._nextAuthMethod = + this._possibleAuthMethods[ + this._possibleAuthMethods.indexOf(this._currentAuthMethod) + 1 + ]; + this.logger.debug(`Current auth method: ${this._currentAuthMethod}`); + + switch (this._currentAuthMethod) { + case "LOGIN": + // LOGIN is a 3 step authentication process + // C: AUTH LOGIN + // C: BASE64(USER) + // C: BASE64(PASS) + this.logger.debug("Authentication via AUTH LOGIN"); + this._currentAction = this._actionAUTH_LOGIN_USER; + this._sendCommand("AUTH LOGIN"); + return; + case "PLAIN": + // AUTH PLAIN is a 1 step authentication process + // C: AUTH PLAIN BASE64(\0 USER \0 PASS) + this.logger.debug("Authentication via AUTH PLAIN"); + this._currentAction = this._actionAUTHComplete; + this._sendCommand( + "AUTH PLAIN " + this._authenticator.getPlainToken(), + true + ); + return; + case "CRAM-MD5": + this.logger.debug("Authentication via AUTH CRAM-MD5"); + this._currentAction = this._actionAUTH_CRAM; + this._sendCommand("AUTH CRAM-MD5"); + return; + case "XOAUTH2": + // See https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange + this.logger.debug("Authentication via AUTH XOAUTH2"); + this._currentAction = this._actionAUTH_XOAUTH2; + let oauthToken = await this._authenticator.getOAuthToken(); + this._sendCommand("AUTH XOAUTH2 " + oauthToken, true); + return; + case "GSSAPI": { + this.logger.debug("Authentication via AUTH GSSAPI"); + this._currentAction = this._actionAUTH_GSSAPI; + this._authenticator.initGssapiAuth("smtp"); + let token; + try { + token = this._authenticator.getNextGssapiToken(""); + } catch (e) { + this.logger.error(e); + this._actionAUTHComplete({ success: false, data: "AUTH GSSAPI" }); + return; + } + this._sendCommand(`AUTH GSSAPI ${token}`, true); + return; + } + case "NTLM": { + this.logger.debug("Authentication via AUTH NTLM"); + this._currentAction = this._actionAUTH_NTLM; + this._authenticator.initNtlmAuth("smtp"); + let token; + try { + token = this._authenticator.getNextNtlmToken(""); + } catch (e) { + this.logger.error(e); + this._actionAUTHComplete({ success: false, data: "AUTH NTLM" }); + return; + } + this._sendCommand(`AUTH NTLM ${token}`, true); + return; + } + } + + this._onAuthFailed({ + data: `Unknown authentication method ${this._currentAuthMethod}`, + }); + } + + _onAuthFailed(command) { + this.logger.error(`Authentication failed: ${command.data}`); + if (!this._freed) { + if (this._nextAuthMethod) { + // Try the next auth method. + this._authenticateUser(); + return; + } else if (!this._currentAuthMethod) { + // No auth method was even tried. + let err; + if ( + this._server.authMethod == Ci.nsMsgAuthMethod.passwordEncrypted && + (this._supportedAuthMethods.includes("PLAIN") || + this._supportedAuthMethods.includes("LOGIN")) + ) { + // Pref has encrypted password, server claims to support plaintext + // password. + err = [ + Ci.nsMsgSocketType.alwaysSTARTTLS, + Ci.nsMsgSocketType.SSL, + ].includes(this._server.socketType) + ? MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL + : MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL; + } else if ( + this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext && + this._supportedAuthMethods.includes("CRAM-MD5") + ) { + // Pref has plaintext password, server claims to support encrypted + // password. + err = MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT; + } else { + err = MsgUtils.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED; + } + this._onNsError(err); + return; + } + } + + // Ask user what to do. + let action = this._authenticator.promptAuthFailed(); + if (action == 1) { + // Cancel button pressed. + this.logger.error(`Authentication failed: ${command.data}`); + this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE); + return; + } else if (action == 2) { + // 'New password' button pressed. Forget cached password, new password + // will be asked. + this._authenticator.forgetPassword(); + } + + if (this._freed) { + // If connection is lost, reconnect. + this.connect(); + return; + } + + // Reset _nextAuthMethod to start again. + this._nextAuthMethod = this._possibleAuthMethods[0]; + if (action == 2 || action == 0) { + // action = 0 means retry button pressed. + this._authenticateUser(); + } + } + + _getHelloArgument() { + let helloArgument = this._server.helloArgument; + if (helloArgument) { + return helloArgument; + } + + try { + // The address format follows rfc5321#section-4.1.3. + let netAddr = this.socket?.transport.getScriptableSelfAddr(); + let address = netAddr.address; + if (netAddr.family === Ci.nsINetAddr.FAMILY_INET6) { + return `[IPV6:${address}]`; + } + return `[${address}]`; + } catch (e) {} + + return "[127.0.0.1]"; + } + + // ACTIONS FOR RESPONSES FROM THE SMTP SERVER + + /** + * Initial response from the server, must have a status 220 + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionGreeting(command) { + if (command.statusCode !== 220) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + return; + } + + if (this.options.lmtp) { + this._currentAction = this._actionLHLO; + this._sendCommand("LHLO " + this._getHelloArgument()); + } else { + this._currentAction = this._actionEHLO; + this._sendCommand("EHLO " + this._getHelloArgument()); + } + } + + /** + * Response to LHLO + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionLHLO(command) { + if (!command.success) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + return; + } + + // Process as EHLO response + this._actionEHLO(command); + } + + /** + * Response to EHLO. If the response is an error, try HELO instead + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionEHLO(command) { + if ([500, 502].includes(command.statusCode)) { + // EHLO is not implemented by the server. + if (this.options.alwaysSTARTTLS) { + // If alwaysSTARTTLS is set by the user, EHLO is required to advertise it. + this._onNsError(MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS); + return; + } + + // Try HELO instead + this.logger.warn( + "EHLO not successful, trying HELO " + this._getHelloArgument() + ); + this._currentAction = this._actionHELO; + this._sendCommand("HELO " + this._getHelloArgument()); + return; + } else if (!command.success) { + // 501 Syntax error or some other error. + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + return; + } + + this._supportedAuthMethods = []; + + let lines = command.data.toUpperCase().split("\n"); + // Skip the first greeting line. + for (let line of lines.slice(1)) { + if (line.startsWith("AUTH ")) { + this._supportedAuthMethods = line.slice(5).split(" "); + } else { + this._capabilities.push(line.split(" ")[0]); + } + } + + if (!this._secureTransport && this.options.alwaysSTARTTLS) { + // STARTTLS is required by the user. Detect if the server supports it. + if (this._capabilities.includes("STARTTLS")) { + this._currentAction = this._actionSTARTTLS; + this._sendCommand("STARTTLS"); + return; + } + // STARTTLS is required but not advertised. + this._onNsError(MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS); + return; + } + + // If a preferred method is not supported by the server, no need to try it. + this._possibleAuthMethods = this._preferredAuthMethods.filter(x => + this._supportedAuthMethods.includes(x) + ); + this.logger.debug(`Possible auth methods: ${this._possibleAuthMethods}`); + this._nextAuthMethod = this._possibleAuthMethods[0]; + + if ( + this._capabilities.includes("CLIENTID") && + (this._secureTransport || + // For test purpose. + ["localhost", "127.0.0.1", "::1"].includes(this._server.hostname)) && + this._server.clientidEnabled && + this._server.clientid + ) { + // Client identity extension, still a draft. + this._currentAction = this._actionCLIENTID; + this._sendCommand("CLIENTID UUID " + this._server.clientid, true); + } else { + this._authenticateUser(); + } + } + + /** + * Handles server response for STARTTLS command. If there's an error + * try HELO instead, otherwise initiate TLS upgrade. If the upgrade + * succeeds restart the EHLO + * + * @param {string} command - Message from the server. + */ + _actionSTARTTLS(command) { + if (!command.success) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + return; + } + + this.socket.upgradeToSecure(); + this._secureTransport = true; + + // restart protocol flow + this._currentAction = this._actionEHLO; + this._sendCommand("EHLO " + this._getHelloArgument()); + } + + /** + * Response to HELO + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionHELO(command) { + if (!command.success) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + return; + } + this._authenticateUser(); + } + + /** + * Handles server response for CLIENTID command. If successful then will + * initiate the authenticateUser process. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionCLIENTID(command) { + if (!command.success) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + return; + } + this._authenticateUser(); + } + + /** + * Returns the saved/cached server password, or show a password dialog. If the + * user cancels the dialog, abort sending. + * + * @returns {string} The server password. + */ + _getPassword() { + try { + return this._authenticator.getPassword(); + } catch (e) { + if (e.result == Cr.NS_ERROR_ABORT) { + this.quit(); + this.onerror(e.result); + } else { + throw e; + } + } + return null; + } + + /** + * Response to AUTH LOGIN, if successful expects base64 encoded username + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionAUTH_LOGIN_USER(command) { + if (command.statusCode !== 334 || command.data !== "VXNlcm5hbWU6") { + this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data); + return; + } + this.logger.debug("AUTH LOGIN USER"); + this._currentAction = this._actionAUTH_LOGIN_PASS; + this._sendCommand(btoa(this._authenticator.username), true); + } + + /** + * Process the response to AUTH LOGIN with a username. If successful, expects + * a base64-encoded password. + * + * @param {{statusCode: number, data: string}} command - Parsed command from + * the server. + */ + _actionAUTH_LOGIN_PASS(command) { + if ( + command.statusCode !== 334 || + (command.data !== btoa("Password:") && command.data !== btoa("password:")) + ) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data); + return; + } + this.logger.debug("AUTH LOGIN PASS"); + this._currentAction = this._actionAUTHComplete; + let password = this._getPassword(); + if ( + !Services.prefs.getBoolPref( + "mail.smtp_login_pop3_user_pass_auth_is_latin1", + true + ) || + !/^[\x00-\xFF]+$/.test(password) // eslint-disable-line no-control-regex + ) { + // Unlike PLAIN auth, the payload of LOGIN auth is not standardized. When + // `mail.smtp_login_pop3_user_pass_auth_is_latin1` is true, we apply + // base64 encoding directly. Otherwise, we convert it to UTF-8 + // BinaryString first. + password = MailStringUtils.stringToByteString(password); + } + this._sendCommand(btoa(password), true); + } + + /** + * Response to AUTH CRAM, if successful expects base64 encoded challenge. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + async _actionAUTH_CRAM(command) { + if (command.statusCode !== 334) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data); + return; + } + this._currentAction = this._actionAUTHComplete; + this._sendCommand( + this._authenticator.getCramMd5Token(this._getPassword(), command.data), + true + ); + } + + /** + * Response to AUTH XOAUTH2 token, if error occurs send empty response + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionAUTH_XOAUTH2(command) { + if (!command.success) { + this.logger.warn("Error during AUTH XOAUTH2, sending empty response"); + this._sendCommand(""); + this._currentAction = this._actionAUTHComplete; + } else { + this._actionAUTHComplete(command); + } + } + + /** + * Response to AUTH GSSAPI, if successful expects a base64 encoded challenge. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionAUTH_GSSAPI(command) { + // GSSAPI auth can be multiple steps. We exchange tokens with the server + // until success or failure. + if (command.success) { + this._actionAUTHComplete(command); + return; + } + if (command.statusCode !== 334) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_GSSAPI, command.data); + return; + } + let token = this._authenticator.getNextGssapiToken(command.data); + this._currentAction = this._actionAUTH_GSSAPI; + this._sendCommand(token, true); + } + + /** + * Response to AUTH NTLM, if successful expects a base64 encoded challenge. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionAUTH_NTLM(command) { + // NTLM auth can be multiple steps. We exchange tokens with the server + // until success or failure. + if (command.success) { + this._actionAUTHComplete(command); + return; + } + if (command.statusCode !== 334) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, command.data); + return; + } + let token = this._authenticator.getNextNtlmToken(command.data); + this._currentAction = this._actionAUTH_NTLM; + this._sendCommand(token, true); + } + + /** + * Checks if authentication succeeded or not. If successfully authenticated + * emit `idle` to indicate that an e-mail can be sent using this connection + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionAUTHComplete(command) { + this._authenticating = false; + if (!command.success) { + this._onAuthFailed(command); + return; + } + + this.logger.debug("Authentication successful."); + + this._currentAction = this._actionIdle; + this.onidle(); // ready to take orders + } + + /** + * Used when the connection is idle, not expecting anything from the server. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionIdle(command) { + this._onNsError(MsgUtils.NS_ERROR_SMTP_SERVER_ERROR, command.data); + } + + /** + * Response to MAIL FROM command. Proceed to defining RCPT TO list if successful + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionMAIL(command) { + if (!command.success) { + let errorCode = MsgUtils.NS_ERROR_SENDING_FROM_COMMAND; // default code + if (command.statusCode == 552) { + // Too much mail data indicated by "size" parameter of MAIL FROM. + // @see https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.9 + errorCode = MsgUtils.NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2; + } + if (command.statusCode == 452 || command.statusCode == 451) { + // @see https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.10 + errorCode = MsgUtils.NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED; + } + this._onNsError(errorCode, command.data, null, command.statusCode); + return; + } + this.logger.debug( + "MAIL FROM successful, proceeding with " + + this._envelope.rcptQueue.length + + " recipients" + ); + this.logger.debug("Adding recipient..."); + this._envelope.curRecipient = this._envelope.rcptQueue.shift(); + this._currentAction = this._actionRCPT; + this._sendCommand( + `RCPT TO:<${this._envelope.curRecipient}>${this._getRCPTParameters()}` + ); + } + + /** + * Prepare the RCPT params, currently only DSN params. If the server supports + * DSN and sender requested DSN, append DSN params to each RCPT TO command. + */ + _getRCPTParameters() { + if (this._capabilities.includes("DSN") && this._envelope.requestDSN) { + let notify = []; + if (Services.prefs.getBoolPref("mail.dsn.request_never_on")) { + notify.push("NEVER"); + } else { + if (Services.prefs.getBoolPref("mail.dsn.request_on_success_on")) { + notify.push("SUCCESS"); + } + if (Services.prefs.getBoolPref("mail.dsn.request_on_failure_on")) { + notify.push("FAILURE"); + } + if (Services.prefs.getBoolPref("mail.dsn.request_on_delay_on")) { + notify.push("DELAY"); + } + } + if (notify.length > 0) { + return ` NOTIFY=${notify.join(",")}`; + } + } + return ""; + } + + /** + * Response to a RCPT TO command. If the command is unsuccessful, emit an + * error to abort the sending. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionRCPT(command) { + if (!command.success) { + this._onNsError( + MsgUtils.NS_ERROR_SENDING_RCPT_COMMAND, + command.data, + this._envelope.curRecipient, + command.statusCode + ); + return; + } + this.rcptCount++; + this._envelope.responseQueue.push(this._envelope.curRecipient); + + if (this._envelope.rcptQueue.length) { + // Send the next recipient. + this._envelope.curRecipient = this._envelope.rcptQueue.shift(); + this._currentAction = this._actionRCPT; + this._sendCommand( + `RCPT TO:<${this._envelope.curRecipient}>${this._getRCPTParameters()}` + ); + } else { + this.logger.debug( + `Total RCPTs during this connection: ${this.rcptCount}` + ); + this.logger.debug("RCPT TO done. Proceeding with payload."); + this._currentAction = this._actionDATA; + this._sendCommand("DATA"); + } + } + + /** + * Response to the DATA command. Server is now waiting for a message, so emit `onready` + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionDATA(command) { + // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24 + // some servers might use 250 instead + if (![250, 354].includes(command.statusCode)) { + this._onNsError(MsgUtils.NS_ERROR_SENDING_DATA_COMMAND, command.data); + return; + } + + this._dataMode = true; + this._currentAction = this._actionIdle; + this.onready(this._envelope.rcptFailed); + } + + /** + * Response from the server, once the message stream has ended with <CR><LF>.<CR><LF> + * Emits `ondone`. + * + * @param {object} command Parsed command from the server {statusCode, data} + */ + _actionStream(command) { + var rcpt; + + if (this.options.lmtp) { + // LMTP returns a response code for *every* successfully set recipient + // For every recipient the message might succeed or fail individually + + rcpt = this._envelope.responseQueue.shift(); + if (!command.success) { + this.logger.error("Local delivery to " + rcpt + " failed."); + this._envelope.rcptFailed.push(rcpt); + } else { + this.logger.error("Local delivery to " + rcpt + " succeeded."); + } + + if (this._envelope.responseQueue.length) { + this._currentAction = this._actionStream; + return; + } + + this._currentAction = this._actionIdle; + this.ondone(0); + } else { + // For SMTP the message either fails or succeeds, there is no information + // about individual recipients + + if (!command.success) { + this.logger.error("Message sending failed."); + } else { + this.logger.debug("Message sent successfully."); + this.isRetry = false; + } + + this._currentAction = this._actionIdle; + if (command.success) { + this.ondone(0); + } else { + this._onNsError(MsgUtils.NS_ERROR_SENDING_MESSAGE, command.data); + } + } + + this._freed = true; + this.onFree(); + } +} diff --git a/comm/mailnews/compose/src/SmtpServer.jsm b/comm/mailnews/compose/src/SmtpServer.jsm new file mode 100644 index 0000000000..3ce81ff936 --- /dev/null +++ b/comm/mailnews/compose/src/SmtpServer.jsm @@ -0,0 +1,519 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["SmtpServer"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + SmtpClient: "resource:///modules/SmtpClient.jsm", +}); + +/** + * This class represents a single SMTP server. + * + * @implements {nsISmtpServer} + * @implements {nsIObserver} + */ + +class SmtpServer { + QueryInterface = ChromeUtils.generateQI(["nsISmtpServer", "nsIObserver"]); + + constructor() { + this._key = ""; + this._loadPrefs(); + + Services.obs.addObserver(this, "passwordmgr-storage-changed"); + } + + /** + * Observe() receives notifications for all accounts, not just this SMTP + * server's * account. So we ignore all notifications not intended for this + * server. When the state of the password manager changes we need to clear the + * this server's password from the cache in case the user just changed or + * removed the password or username. + * OAuth2 servers often automatically change the password manager's stored + * password (the token). + */ + observe(subject, topic, data) { + if (topic == "passwordmgr-storage-changed") { + // Check that the notification is for this server and user. + let otherFullName = ""; + let otherUsername = ""; + if (subject instanceof Ci.nsILoginInfo) { + // The login info for a server has been removed with aData being + // "removeLogin" or "removeAllLogins". + otherFullName = subject.origin; + otherUsername = subject.username; + } else if (subject instanceof Ci.nsIArray) { + // Probably a 2 element array containing old and new login info due to + // aData being "modifyLogin". E.g., a user has modified the password or + // username in the password manager or an OAuth2 token string has + // automatically changed. Only need to look at names in first array + // element (login info before any modification) since the user might + // have changed the username as found in the 2nd elements. (The + // hostname can't be modified in the password manager. + otherFullName = subject.queryElementAt(0, Ci.nsISupports).origin; + otherUsername = subject.queryElementAt(0, Ci.nsISupports).username; + } + if (otherFullName) { + if ( + otherFullName != "smtp://" + this.hostname || + otherUsername != this.username + ) { + // Not for this account; keep this account's password. + return; + } + } else if (data != "hostSavingDisabled") { + // "hostSavingDisabled" only occurs during test_smtpServer.js and + // expects the password to be removed from memory cache. Otherwise, we + // don't have enough information to decide to remove the cached + // password, so keep it. + return; + } + // Remove the password for this server cached in memory. + this.password = ""; + } + } + + get key() { + return this._key; + } + + set key(key) { + this._key = key; + this._loadPrefs(); + } + + get UID() { + let uid = this._prefs.getStringPref("uid", ""); + if (uid) { + return uid; + } + return (this.UID = Services.uuid + .generateUUID() + .toString() + .substring(1, 37)); + } + + set UID(uid) { + if (this._prefs.prefHasUserValue("uid")) { + throw new Components.Exception("uid is already set", Cr.NS_ERROR_ABORT); + } + this._prefs.setStringPref("uid", uid); + } + + get description() { + return this._prefs.getStringPref("description", ""); + } + + set description(value) { + this._prefs.setStringPref("description", value); + } + + get hostname() { + return this._prefs.getStringPref("hostname", ""); + } + + set hostname(value) { + if (value.toLowerCase() != this.hostname.toLowerCase()) { + // Reset password so that users are prompted for new password for the new + // host. + this.forgetPassword(); + } + this._prefs.setStringPref("hostname", value); + } + + get port() { + return this._prefs.getIntPref("port", 0); + } + + set port(value) { + if (value) { + this._prefs.setIntPref("port", value); + } else { + this._prefs.clearUserPref("port"); + } + } + + get displayname() { + return `${this.hostname}` + (this.port ? `:${this.port}` : ""); + } + + get username() { + return this._prefs.getCharPref("username", ""); + } + + set username(value) { + if (value != this.username) { + // Reset password so that users are prompted for new password for the new + // username. + this.forgetPassword(); + } + this._setCharPref("username", value); + } + + get clientid() { + return this._getCharPrefWithDefault("clientid"); + } + + set clientid(value) { + this._setCharPref("clientid", value); + } + + get clientidEnabled() { + try { + return this._prefs.getBoolPref("clientidEnabled"); + } catch (e) { + return this._defaultPrefs.getBoolPref("clientidEnabled", false); + } + } + + set clientidEnabled(value) { + this._prefs.setBoolPref("clientidEnabled", value); + } + + get authMethod() { + return this._getIntPrefWithDefault("authMethod", 3); + } + + set authMethod(value) { + this._prefs.setIntPref("authMethod", value); + } + + get socketType() { + return this._getIntPrefWithDefault("try_ssl", 0); + } + + set socketType(value) { + this._prefs.setIntPref("try_ssl", value); + } + + get helloArgument() { + return this._getCharPrefWithDefault("hello_argument"); + } + + get serverURI() { + return this._getServerURI(true); + } + + /** + * If pref max_cached_connection is set to less than 1, allow only one + * connection and one message to be sent on that connection. Otherwise, allow + * up to max_cached_connection (default to 3) with each connection allowed to + * send multiple messages. + */ + get maximumConnectionsNumber() { + let maxConnections = this._getIntPrefWithDefault( + "max_cached_connections", + 3 + ); + // Always return a value >= 0. + return maxConnections > 0 ? maxConnections : 0; + } + + set maximumConnectionsNumber(value) { + this._prefs.setIntPref("max_cached_connections", value); + } + + get password() { + if (this._password) { + return this._password; + } + let incomingAccountKey = this._prefs.getCharPref("incomingAccount", ""); + let incomingServer; + if (incomingAccountKey) { + incomingServer = + MailServices.accounts.getIncomingServer(incomingAccountKey); + } else { + let useMatchingHostNameServer = Services.prefs.getBoolPref( + "mail.smtp.useMatchingHostNameServer" + ); + let useMatchingDomainServer = Services.prefs.getBoolPref( + "mail.smtp.useMatchingDomainServer" + ); + if (useMatchingHostNameServer || useMatchingDomainServer) { + if (useMatchingHostNameServer) { + // Pass in empty type and port=0, to match imap and pop3. + incomingServer = MailServices.accounts.findServer( + this.username, + this.hostname, + "", + 0 + ); + } + if ( + !incomingServer && + useMatchingDomainServer && + this.hostname.includes(".") + ) { + let newHostname = this.hostname.slice(0, this.hostname.indexOf(".")); + for (let server of MailServices.accounts.allServers) { + if (server.username == this.username) { + let serverHostName = server.hostName; + if ( + serverHostName.includes(".") && + serverHostName.slice(0, serverHostName.indexOf(".")) == + newHostname + ) { + incomingServer = server; + break; + } + } + } + } + } + } + return incomingServer?.password || ""; + } + + set password(password) { + this._password = password; + } + + getPasswordWithUI(promptMessage, promptTitle) { + let authPrompt; + try { + // This prompt has a checkbox for saving password. + authPrompt = Cc["@mozilla.org/messenger/msgAuthPrompt;1"].getService( + Ci.nsIAuthPrompt + ); + } catch (e) { + // Often happens in tests. This prompt has no checkbox for saving password. + authPrompt = Services.ww.getNewAuthPrompter(null); + } + let password = this._getPasswordWithoutUI(); + if (password) { + this.password = password; + return this.password; + } + let outUsername = {}; + let outPassword = {}; + let ok; + if (this.username) { + ok = authPrompt.promptPassword( + promptTitle, + promptMessage, + this.serverURI, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + outPassword + ); + } else { + ok = authPrompt.promptUsernameAndPassword( + promptTitle, + promptMessage, + this.serverURI, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + outUsername, + outPassword + ); + } + if (ok) { + if (outUsername.value) { + this.username = outUsername.value; + } + this.password = outPassword.value; + } else { + throw Components.Exception("Password dialog canceled", Cr.NS_ERROR_ABORT); + } + return this.password; + } + + forgetPassword() { + let serverURI = this._getServerURI(); + let logins = Services.logins.findLogins(serverURI, "", serverURI); + for (let login of logins) { + if (login.username == this.username) { + Services.logins.removeLogin(login); + } + } + this.password = ""; + } + + verifyLogon(urlListener, msgWindow) { + return MailServices.smtp.verifyLogon(this, urlListener, msgWindow); + } + + clearAllValues() { + for (let prefName of this._prefs.getChildList("")) { + this._prefs.clearUserPref(prefName); + } + } + + /** + * @returns {string} + */ + _getPasswordWithoutUI() { + let serverURI = this._getServerURI(); + let logins = Services.logins.findLogins(serverURI, "", serverURI); + for (let login of logins) { + if (login.username == this.username) { + return login.password; + } + } + return null; + } + + /** + * Get server URI in the form of smtp://[user@]hostname. + * + * @param {boolean} includeUsername - Whether to include the username. + * @returns {string} + */ + _getServerURI(includeUsername) { + // When constructing nsIURI, need to wrap IPv6 address in []. + let hostname = this.hostname.includes(":") + ? `[${this.hostname}]` + : this.hostname; + return ( + "smtp://" + + (includeUsername && this.username + ? `${encodeURIComponent(this.username)}@` + : "") + + hostname + ); + } + + /** + * Get the associated pref branch and the default SMTP server branch. + */ + _loadPrefs() { + this._prefs = Services.prefs.getBranch(`mail.smtpserver.${this._key}.`); + this._defaultPrefs = Services.prefs.getBranch("mail.smtpserver.default."); + } + + /** + * Set or clear a string preference. + * + * @param {string} name - The preference name. + * @param {string} value - The preference value. + */ + _setCharPref(name, value) { + if (value) { + this._prefs.setCharPref(name, value); + } else { + this._prefs.clearUserPref(name); + } + } + + /** + * Get the value of a char preference from this or default SMTP server. + * + * @param {string} name - The preference name. + * @param {number} [defaultValue=""] - The default value to return. + * @returns {string} + */ + _getCharPrefWithDefault(name, defaultValue = "") { + try { + return this._prefs.getCharPref(name); + } catch (e) { + return this._defaultPrefs.getCharPref(name, defaultValue); + } + } + + /** + * Get the value of an integer preference from this or default SMTP server. + * + * @param {string} name - The preference name. + * @param {number} defaultValue - The default value to return. + * @returns {number} + */ + _getIntPrefWithDefault(name, defaultValue) { + try { + return this._prefs.getIntPref(name); + } catch (e) { + return this._defaultPrefs.getIntPref(name, defaultValue); + } + } + + get wrappedJSObject() { + return this; + } + + // @type {SmtpClient[]} - An array of connections can be used. + _freeConnections = []; + // @type {SmtpClient[]} - An array of connections in use. + _busyConnections = []; + // @type {Function[]} - An array of Promise.resolve functions. + _connectionWaitingQueue = []; + + closeCachedConnections() { + // Close all connections. + for (let client of [...this._freeConnections, ...this._busyConnections]) { + client.quit(); + } + // Cancel all waitings in queue. + for (let resolve of this._connectionWaitingQueue) { + resolve(false); + } + this._freeConnections = []; + this._busyConnections = []; + } + + /** + * Get an idle connection that can be used. + * + * @returns {SmtpClient} + */ + async _getNextClient() { + // The newest connection is the least likely to have timed out. + let client = this._freeConnections.pop(); + if (client) { + this._busyConnections.push(client); + return client; + } + const maxConns = this.maximumConnectionsNumber + ? this.maximumConnectionsNumber + : 1; + if ( + this._freeConnections.length + this._busyConnections.length < + maxConns + ) { + // Create a new client if the pool is not full. + client = new lazy.SmtpClient(this); + this._busyConnections.push(client); + return client; + } + // Wait until a connection is available. + await new Promise(resolve => this._connectionWaitingQueue.push(resolve)); + return this._getNextClient(); + } + /** + * Do some actions with a connection. + * + * @param {Function} handler - A callback function to take a SmtpClient + * instance, and do some actions. + */ + async withClient(handler) { + let client = await this._getNextClient(); + client.onFree = () => { + this._busyConnections = this._busyConnections.filter(c => c != client); + // Per RFC, the minimum total number of recipients that MUST be buffered + // is 100 recipients. + // @see https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.8 + // So use a new connection for the next message to avoid running into + // recipient limits. + // If user has set SMTP pref max_cached_connection to less than 1, + // use a new connection for each message. + if (this.maximumConnectionsNumber == 0 || client.rcptCount > 99) { + // Send QUIT, server will then terminate the connection + client.quit(); + } else { + // Keep using this connection + this._freeConnections.push(client); + // Resolve the first waiting in queue. + this._connectionWaitingQueue.shift()?.(); + } + }; + handler(client); + client.connect(); + } +} diff --git a/comm/mailnews/compose/src/SmtpService.jsm b/comm/mailnews/compose/src/SmtpService.jsm new file mode 100644 index 0000000000..d90983a213 --- /dev/null +++ b/comm/mailnews/compose/src/SmtpService.jsm @@ -0,0 +1,350 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["SmtpService"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; +XPCOMUtils.defineLazyModuleGetters(lazy, { + SmtpClient: "resource:///modules/SmtpClient.jsm", + MsgUtils: "resource:///modules/MimeMessageUtils.jsm", +}); + +/** + * The SMTP service. + * + * @implements {nsISmtpService} + */ +class SmtpService { + QueryInterface = ChromeUtils.generateQI(["nsISmtpService"]); + + constructor() { + this._servers = []; + this._logger = lazy.MsgUtils.smtpLogger; + } + + /** + * @see nsISmtpService + */ + get defaultServer() { + let defaultServerKey = Services.prefs.getCharPref( + "mail.smtp.defaultserver", + "" + ); + if (defaultServerKey) { + // Get it from the prefs. + return this.getServerByKey(defaultServerKey); + } + + // No pref set, so set the first one as default, and return it. + if (this.servers.length > 0) { + this.defaultServer = this.servers[0]; + return this.servers[0]; + } + return null; + } + + set defaultServer(server) { + Services.prefs.setCharPref("mail.smtp.defaultserver", server.key); + } + + get servers() { + if (!this._servers.length) { + // Load SMTP servers from prefs. + this._servers = this._getSmtpServerKeys().map(key => + this._keyToServer(key) + ); + } + return this._servers; + } + + get wrappedJSObject() { + return this; + } + + /** + * @see nsISmtpService + */ + async sendMailMessage( + messageFile, + recipients, + userIdentity, + sender, + password, + deliveryListener, + statusListener, + notificationCallbacks, + requestDSN, + messageId, + outURI, + outRequest + ) { + this._logger.debug(`Sending message ${messageId}`); + let server = this.getServerByIdentity(userIdentity); + if (!server) { + // Occurs for at least one unit test, but test does not fail if return + // here. This check for "server" can be removed if tests are fixed. + console.log( + `No server found for identity with email ${userIdentity.email} and ` + + `smtpServerKey ${userIdentity.smtpServerKey}` + ); + return; + } + if (password) { + server.password = password; + } + let runningUrl = this._getRunningUri(server); + await server.wrappedJSObject.withClient(client => { + deliveryListener?.OnStartRunningUrl(runningUrl, 0); + let fresh = true; + client.onidle = () => { + // onidle can occur multiple times, but we should only init sending + // when sending a new message(fresh is true) or when a new connection + // replaces the original connection due to error 4xx response + // (client.isRetry is true). + if (!fresh && !client.isRetry) { + return; + } + // Init when fresh==true OR re-init sending when client.isRetry==true. + fresh = false; + let from = sender; + let to = MailServices.headerParser + .parseEncodedHeaderW(decodeURIComponent(recipients)) + .map(rec => rec.email); + + if ( + !Services.prefs.getBoolPref( + "mail.smtp.useSenderForSmtpMailFrom", + false + ) + ) { + from = userIdentity.email; + } + if (!messageId) { + messageId = Cc["@mozilla.org/messengercompose/computils;1"] + .createInstance(Ci.nsIMsgCompUtils) + .msgGenerateMessageId(userIdentity, null); + } + client.useEnvelope({ + from: MailServices.headerParser.parseEncodedHeaderW( + decodeURIComponent(from) + )[0].email, + to, + size: messageFile.fileSize, + requestDSN, + messageId, + }); + }; + let socketOnDrain; + client.onready = async () => { + let fstream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + // PR_RDONLY + fstream.init(messageFile, 0x01, 0, 0); + + let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sstream.init(fstream); + + let sentSize = 0; + let totalSize = messageFile.fileSize; + let progressListener = statusListener?.QueryInterface( + Ci.nsIWebProgressListener + ); + + while (sstream.available()) { + let chunk = sstream.read(65536); + let canSendMore = client.send(chunk); + if (!canSendMore) { + // Socket buffer is full, wait for the ondrain event. + await new Promise(resolve => (socketOnDrain = resolve)); + } + // In practice, chunks are buffered by TCPSocket, progress reaches 100% + // almost immediately unless message is larger than chunk size. + sentSize += chunk.length; + progressListener?.onProgressChange( + null, + null, + sentSize, + totalSize, + sentSize, + totalSize + ); + } + sstream.close(); + fstream.close(); + client.end(); + + // Set progress to indeterminate. + progressListener?.onProgressChange(null, null, 0, -1, 0, -1); + }; + client.ondrain = () => { + // Socket buffer is empty, safe to continue sending. + socketOnDrain(); + }; + client.ondone = exitCode => { + if (!AppConstants.MOZ_SUITE) { + Services.telemetry.scalarAdd("tb.mails.sent", 1); + } + deliveryListener?.OnStopRunningUrl(runningUrl, exitCode); + }; + client.onerror = (nsError, errorMessage, secInfo) => { + runningUrl.QueryInterface(Ci.nsIMsgMailNewsUrl); + if (secInfo) { + // TODO(emilio): Passing the failed security info as part of the URI is + // quite a smell, but monkey see monkey do... + runningUrl.failedSecInfo = secInfo; + } + runningUrl.errorMessage = errorMessage; + deliveryListener?.OnStopRunningUrl(runningUrl, nsError); + }; + + outRequest.value = { + cancel() { + client.close(true); + }, + }; + }); + } + + /** + * @see nsISmtpService + */ + verifyLogon(server, urlListener, msgWindow) { + let client = new lazy.SmtpClient(server); + client.connect(); + let runningUrl = this._getRunningUri(server); + client.onerror = (nsError, errorMessage, secInfo) => { + runningUrl.QueryInterface(Ci.nsIMsgMailNewsUrl); + if (secInfo) { + runningUrl.failedSecInfo = secInfo; + } + runningUrl.errorMessage = errorMessage; + urlListener.OnStopRunningUrl(runningUrl, nsError); + }; + client.onready = () => { + urlListener.OnStopRunningUrl(runningUrl, 0); + client.close(); + }; + return runningUrl; + } + + /** + * @see nsISmtpService + */ + getServerByIdentity(userIdentity) { + return userIdentity.smtpServerKey + ? this.getServerByKey(userIdentity.smtpServerKey) + : this.defaultServer; + } + + /** + * @see nsISmtpService + */ + getServerByKey(key) { + return this.servers.find(s => s.key == key); + } + + /** + * @see nsISmtpService + */ + createServer() { + let serverKeys = this._getSmtpServerKeys(); + let i = 1; + let key; + do { + key = `smtp${i++}`; + } while (serverKeys.includes(key)); + + serverKeys.push(key); + this._saveSmtpServerKeys(serverKeys); + this._servers = []; // Reset to force repopulation of this.servers. + return this.servers.at(-1); + } + + /** + * @see nsISmtpService + */ + deleteServer(server) { + let serverKeys = this._getSmtpServerKeys().filter(k => k != server.key); + this._servers = this.servers.filter(s => s.key != server.key); + this._saveSmtpServerKeys(serverKeys); + } + + /** + * @see nsISmtpService + */ + findServer(username, hostname) { + username = username?.toLowerCase(); + hostname = hostname?.toLowerCase(); + return this.servers.find(server => { + if ( + (username && server.username.toLowerCase() != username) || + (hostname && server.hostname.toLowerCase() != hostname) + ) { + return false; + } + return true; + }); + } + + /** + * Get all SMTP server keys from prefs. + * + * @returns {string[]} + */ + _getSmtpServerKeys() { + return Services.prefs + .getCharPref("mail.smtpservers", "") + .split(",") + .filter(Boolean); + } + + /** + * Save SMTP server keys to prefs. + * + * @param {string[]} keys - The key list to save. + */ + _saveSmtpServerKeys(keys) { + return Services.prefs.setCharPref("mail.smtpservers", keys.join(",")); + } + + /** + * Create an nsISmtpServer from a key. + * + * @param {string} key - The key for the SmtpServer. + * @returns {nsISmtpServer} + */ + _keyToServer(key) { + let server = Cc["@mozilla.org/messenger/smtp/server;1"].createInstance( + Ci.nsISmtpServer + ); + // Setting the server key will set up all of its other properties by + // reading them from the prefs. + server.key = key; + return server; + } + + /** + * Get the server URI in the form of smtp://user@hostname:port. + * + * @param {nsISmtpServer} server - The SMTP server. + * @returns {nsIURI} + */ + _getRunningUri(server) { + let spec = server.serverURI + (server.port ? `:${server.port}` : ""); + return Services.io.newURI(spec); + } +} diff --git a/comm/mailnews/compose/src/components.conf b/comm/mailnews/compose/src/components.conf new file mode 100644 index 0000000000..44754289f5 --- /dev/null +++ b/comm/mailnews/compose/src/components.conf @@ -0,0 +1,197 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +Classes = [ + { + "cid": "{588595fe-1ada-11d3-a715-0060b0eb39b5}", + "contract_ids": [ + "@mozilla.org/messengercompose;1", + "@mozilla.org/commandlinehandler/general-startup;1?type=compose", + ], + "type": "nsMsgComposeService", + "init_method": "Init", + "headers": ["/comm/mailnews/compose/src/nsMsgComposeService.h"], + "categories": {"command-line-handler": "m-compose"}, + "name": "Compose", + "interfaces": ["nsIMsgComposeService"], + }, + { + "cid": "{0b63fb80-bbba-11d4-9daa-91b657eb313c}", + "contract_ids": [ + "@mozilla.org/uriloader/content-handler;1?type=application/x-mailto" + ], + "type": "nsMsgComposeContentHandler", + "headers": ["/comm/mailnews/compose/src/nsMsgComposeContentHandler.h"], + }, + { + "cid": "{eb5bdaf8-bbc6-11d2-a6ec-0060b0eb39b5}", + "contract_ids": ["@mozilla.org/messengercompose/compose;1"], + "type": "nsMsgCompose", + "headers": ["/comm/mailnews/compose/src/nsMsgCompose.h"], + }, + { + "cid": "{cb998a00-c079-11d4-9daa-8df64bab2efc}", + "contract_ids": ["@mozilla.org/messengercompose/composeparams;1"], + "type": "nsMsgComposeParams", + "headers": ["/comm/mailnews/compose/src/nsMsgComposeParams.h"], + }, + { + "cid": "{acc72781-2cea-11d5-9daa-bacdeac1eefc}", + "contract_ids": ["@mozilla.org/messengercompose/composesendlistener;1"], + "type": "nsMsgComposeSendListener", + "headers": ["/comm/mailnews/compose/src/nsMsgCompose.h"], + }, + { + "cid": "{1e0e7c01-3e4c-11d5-9daa-f88d288130fc}", + "contract_ids": ["@mozilla.org/messengercompose/composeprogressparameters;1"], + "type": "nsMsgComposeProgressParams", + "headers": ["/comm/mailnews/compose/src/nsMsgComposeProgressParams.h"], + }, + { + "cid": "{e64b0f51-0d7b-4e2f-8c60-3862ee8c174f}", + "contract_ids": ["@mozilla.org/messengercompose/composefields;1"], + "type": "nsMsgCompFields", + "headers": ["/comm/mailnews/compose/src/nsMsgCompFields.h"], + }, + { + "cid": "{27b8d045-8d9f-4fa8-bfb6-8a0f8d09ce89}", + "contract_ids": ["@mozilla.org/messengercompose/attachment;1"], + "type": "nsMsgAttachment", + "headers": ["/comm/mailnews/compose/src/nsMsgAttachment.h"], + }, + { + "cid": "{9e16958d-d9e9-4cae-b723-a5bccf104998}", + "contract_ids": ["@mozilla.org/messengercompose/attachmentdata;1"], + "type": "nsMsgAttachmentData", + "headers": ["/comm/mailnews/compose/src/nsMsgAttachmentData.h"], + }, + { + "cid": "{ef173501-4e14-42b9-ae1f-7770de235c29}", + "contract_ids": ["@mozilla.org/messengercompose/attachedfile;1"], + "type": "nsMsgAttachedFile", + "headers": ["/comm/mailnews/compose/src/nsMsgAttachedFile.h"], + }, + { + "cid": "{e15c83f1-1cf4-11d3-8ef0-00a024a7d144}", + "contract_ids": ["@mozilla.org/messengercompose/sendlater;1"], + "type": "nsMsgSendLater", + "init_method": "Init", + "headers": ["/comm/mailnews/compose/src/nsMsgSendLater.h"], + }, + { + "cid": "{be59dbf0-2812-11d3-80a3-006008128c4e}", + "contract_ids": ["@mozilla.org/messengercompose/smtpurl;1"], + "type": "nsSmtpUrl", + "headers": ["/comm/mailnews/compose/src/nsSmtpUrl.h"], + }, + { + "cid": "{05bab5e7-9c7d-11d3-98a3-001083010e9b}", + "contract_ids": ["@mozilla.org/messengercompose/mailtourl;1"], + "type": "nsMailtoUrl", + "headers": ["/comm/mailnews/compose/src/nsSmtpUrl.h"], + }, + { + "cid": "{1c7abf0c-21e5-11d3-8ef1-00a024a7d144}", + "contract_ids": ["@mozilla.org/messengercompose/quoting;1"], + "type": "nsMsgQuote", + "headers": ["/comm/mailnews/compose/src/nsMsgQuote.h"], + }, + { + "cid": "{683728ac-88df-11d3-989d-001083010e9b}", + "contract_ids": ["@mozilla.org/messengercompose/quotinglistener;1"], + "type": "nsMsgQuoteListener", + "headers": ["/comm/mailnews/compose/src/nsMsgQuote.h"], + }, + { + "cid": "{ceb0dca2-5e7d-4204-94d4-2ab925921fae}", + "contract_ids": ["@mozilla.org/messengercompose/computils;1"], + "type": "nsMsgCompUtils", + "headers": ["/comm/mailnews/compose/src/nsMsgCompUtils.h"], + }, + { + "cid": "{0874c3b5-317d-11d3-8efb-00a024a7d144}", + "contract_ids": ["@mozilla.org/messengercompose/msgcopy;1"], + "type": "nsMsgCopy", + "headers": ["/comm/mailnews/compose/src/nsMsgCopy.h"], + }, + { + "cid": "{e5872045-a87b-4ea0-b366-45ebd7dc89d9}", + "contract_ids": ["@mozilla.org/messengercompose/sendreport;1"], + "type": "nsMsgSendReport", + "headers": ["/comm/mailnews/compose/src/nsMsgSendReport.h"], + }, + { + "cid": "{028b9c1e-8d0a-4518-80c2-842e07846eaa}", + "contract_ids": ["@mozilla.org/messengercompose/send;1"], + "jsm": "resource:///modules/MessageSend.jsm", + "constructor": "MessageSend", + }, + { + "cid": "{b14c2b67-8680-4c11-8d63-9403c7d4f757}", + "contract_ids": ["@mozilla.org/network/protocol;1?name=smtp"], + "jsm": "resource:///modules/SMTPProtocolHandler.jsm", + "constructor": "SMTPProtocolHandler", + "protocol_config": { + "scheme": "smtp", + "flags": [ + "URI_NORELATIVE", + "URI_DANGEROUS_TO_LOAD", + "ALLOWS_PROXY", + "URI_NON_PERSISTABLE", + "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT", + ], + "default_port": 25, + }, + }, + { + "cid": "{057d0997-9e3a-411e-b4ee-2602f53fe05f}", + "contract_ids": ["@mozilla.org/network/protocol;1?name=smtps"], + "jsm": "resource:///modules/SMTPProtocolHandler.jsm", + "constructor": "SMTPSProtocolHandler", + "protocol_config": { + "scheme": "smtps", + "flags": [ + "URI_NORELATIVE", + "URI_DANGEROUS_TO_LOAD", + "ALLOWS_PROXY", + "URI_NON_PERSISTABLE", + "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT", + ], + "default_port": 465, + }, + }, + { + "cid": "{af314bd9-0b28-4f69-9bea-592ab4dc6811}", + "contract_ids": ["@mozilla.org/network/protocol;1?name=mailto"], + "jsm": "resource:///modules/MailtoProtocolHandler.jsm", + "constructor": "MailtoProtocolHandler", + "protocol_config": { + "scheme": "mailto", + "flags": [ + "URI_NORELATIVE", + "ALLOWS_PROXY", + "URI_LOADABLE_BY_ANYONE", + "URI_NON_PERSISTABLE", + "URI_DOES_NOT_RETURN_DATA", + "URI_FORBIDS_COOKIE_ACCESS", + ], + }, + }, + { + "cid": "{acda6039-8b17-46c1-a8ed-ad50aa80f412}", + "contract_ids": ["@mozilla.org/messengercompose/smtp;1"], + "jsm": "resource:///modules/SmtpService.jsm", + "constructor": "SmtpService", + "name": "Smtp", + "interfaces": ["nsISmtpService"], + }, + { + "cid": "{3a75f5ea-651e-4696-9813-848c03da8bbd}", + "contract_ids": ["@mozilla.org/messenger/smtp/server;1"], + "jsm": "resource:///modules/SmtpServer.jsm", + "constructor": "SmtpServer", + }, +] diff --git a/comm/mailnews/compose/src/moz.build b/comm/mailnews/compose/src/moz.build new file mode 100644 index 0000000000..60ff148540 --- /dev/null +++ b/comm/mailnews/compose/src/moz.build @@ -0,0 +1,60 @@ +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +EXPORTS += [ + "nsComposeStrings.h", + "nsMsgAttachmentData.h", + "nsMsgCompFields.h", + "nsMsgCompose.h", +] + +SOURCES += [ + "nsComposeStrings.cpp", + "nsMsgAttachedFile.cpp", + "nsMsgAttachment.cpp", + "nsMsgAttachmentData.cpp", + "nsMsgCompFields.cpp", + "nsMsgCompose.cpp", + "nsMsgComposeContentHandler.cpp", + "nsMsgComposeParams.cpp", + "nsMsgComposeProgressParams.cpp", + "nsMsgComposeService.cpp", + "nsMsgCompUtils.cpp", + "nsMsgCopy.cpp", + "nsMsgPrompts.cpp", + "nsMsgQuote.cpp", + "nsMsgSendLater.cpp", + "nsMsgSendReport.cpp", + "nsSmtpUrl.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "mail" + +# clang-cl rightly complains about switch on nsresult. +if CONFIG["CC_TYPE"] == "clang-cl": + CXXFLAGS += ["-Wno-switch"] + +EXTRA_JS_MODULES += [ + "MailtoProtocolHandler.jsm", + "MessageSend.jsm", + "MimeEncoder.jsm", + "MimeMessage.jsm", + "MimeMessageUtils.jsm", + "MimePart.jsm", + "SmtpClient.jsm", + "SMTPProtocolHandler.jsm", + "SmtpServer.jsm", + "SmtpService.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mailnews/compose/src/nsComposeStrings.cpp b/comm/mailnews/compose/src/nsComposeStrings.cpp new file mode 100644 index 0000000000..666cd14e07 --- /dev/null +++ b/comm/mailnews/compose/src/nsComposeStrings.cpp @@ -0,0 +1,106 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsComposeStrings.h" + +const char* errorStringNameForErrorCode(nsresult aCode) { +#ifdef __GNUC__ +// Temporary workaround until bug 783526 is fixed. +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wswitch" +#endif + switch (aCode) { + case NS_MSG_UNABLE_TO_OPEN_FILE: + return "unableToOpenFile"; + case NS_MSG_UNABLE_TO_OPEN_TMP_FILE: + return "unableToOpenTmpFile"; + case NS_MSG_UNABLE_TO_SAVE_TEMPLATE: + return "unableToSaveTemplate"; + case NS_MSG_UNABLE_TO_SAVE_DRAFT: + return "unableToSaveDraft"; + case NS_MSG_COULDNT_OPEN_FCC_FOLDER: + return "couldntOpenFccFolder"; + case NS_MSG_NO_SENDER: + return "noSender"; + case NS_MSG_NO_RECIPIENTS: + return "noRecipients"; + case NS_MSG_ERROR_WRITING_FILE: + return "errorWritingFile"; + case NS_ERROR_SENDING_FROM_COMMAND: + return "errorSendingFromCommand"; + case NS_ERROR_SENDING_DATA_COMMAND: + return "errorSendingDataCommand"; + case NS_ERROR_SENDING_MESSAGE: + return "errorSendingMessage"; + case NS_ERROR_POST_FAILED: + return "postFailed"; + case NS_ERROR_SMTP_SERVER_ERROR: + return "smtpServerError"; + case NS_MSG_UNABLE_TO_SEND_LATER: + return "unableToSendLater"; + case NS_ERROR_COMMUNICATIONS_ERROR: + return "communicationsError"; + case NS_ERROR_BUT_DONT_SHOW_ALERT: + return "dontShowAlert"; + case NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS: + return "couldNotGetUsersMailAddress2"; + case NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY: + return "couldNotGetSendersIdentity"; + case NS_ERROR_MIME_MPART_ATTACHMENT_ERROR: + return "mimeMpartAttachmentError"; + case NS_ERROR_NNTP_NO_CROSS_POSTING: + return "nntpNoCrossPosting"; + case NS_MSG_ERROR_READING_FILE: + return "errorReadingFile"; + case NS_MSG_ERROR_ATTACHING_FILE: + return "errorAttachingFile"; + case NS_ERROR_SMTP_GREETING: + return "incorrectSmtpGreeting"; + case NS_ERROR_SENDING_RCPT_COMMAND: + return "errorSendingRcptCommand"; + case NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS: + return "startTlsFailed"; + case NS_ERROR_SMTP_PASSWORD_UNDEFINED: + return "smtpPasswordUndefined"; + case NS_ERROR_SMTP_SEND_NOT_ALLOWED: + return "smtpSendNotAllowed"; + case NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED: + return "smtpTooManyRecipients"; + case NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2: + return "smtpPermSizeExceeded2"; + case NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER: + return "smtpSendFailedUnknownServer"; + case NS_ERROR_SMTP_SEND_FAILED_REFUSED: + return "smtpSendRequestRefused"; + case NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED: + return "smtpSendInterrupted"; + case NS_ERROR_SMTP_SEND_FAILED_TIMEOUT: + return "smtpSendTimeout"; + case NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON: + return "smtpSendFailedUnknownReason"; + case NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL: + return "smtpHintAuthEncryptToPlainNoSsl"; + case NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL: + return "smtpHintAuthEncryptToPlainSsl"; + case NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT: + return "smtpHintAuthPlainToEncrypt"; + case NS_ERROR_SMTP_AUTH_FAILURE: + return "smtpAuthFailure"; + case NS_ERROR_SMTP_AUTH_GSSAPI: + return "smtpAuthGssapi"; + case NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED: + return "smtpAuthMechNotSupported"; + case NS_ERROR_ILLEGAL_LOCALPART: + return "errorIllegalLocalPart2"; + case NS_ERROR_CLIENTID: + return "smtpClientid"; + case NS_ERROR_CLIENTID_PERMISSION: + return "smtpClientidPermission"; + default: + return "sendFailed"; + } +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif +} diff --git a/comm/mailnews/compose/src/nsComposeStrings.h b/comm/mailnews/compose/src/nsComposeStrings.h new file mode 100644 index 0000000000..3d6a24516f --- /dev/null +++ b/comm/mailnews/compose/src/nsComposeStrings.h @@ -0,0 +1,76 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// clang-format off +/** + String Ids used by mailnews\compose + To Do: Convert the callers to use names instead of ids and then make this file obsolete. + */ + +#ifndef _nsComposeStrings_H__ +#define _nsComposeStrings_H__ + +#include "msgCore.h" + +#define NS_MSG_UNABLE_TO_OPEN_FILE NS_MSG_GENERATE_FAILURE(12500) +#define NS_MSG_UNABLE_TO_OPEN_TMP_FILE NS_MSG_GENERATE_FAILURE(12501) +#define NS_MSG_UNABLE_TO_SAVE_TEMPLATE NS_MSG_GENERATE_FAILURE(12502) +#define NS_MSG_UNABLE_TO_SAVE_DRAFT NS_MSG_GENERATE_FAILURE(12503) +#define NS_MSG_COULDNT_OPEN_FCC_FOLDER NS_MSG_GENERATE_FAILURE(12506) +#define NS_MSG_NO_SENDER NS_MSG_GENERATE_FAILURE(12510) +#define NS_MSG_NO_RECIPIENTS NS_MSG_GENERATE_FAILURE(12511) +#define NS_MSG_ERROR_WRITING_FILE NS_MSG_GENERATE_FAILURE(12512) +#define NS_ERROR_SENDING_FROM_COMMAND NS_MSG_GENERATE_FAILURE(12514) +#define NS_ERROR_SENDING_DATA_COMMAND NS_MSG_GENERATE_FAILURE(12516) +#define NS_ERROR_SENDING_MESSAGE NS_MSG_GENERATE_FAILURE(12517) +#define NS_ERROR_POST_FAILED NS_MSG_GENERATE_FAILURE(12518) +#define NS_ERROR_SMTP_SERVER_ERROR NS_MSG_GENERATE_FAILURE(12524) +#define NS_MSG_UNABLE_TO_SEND_LATER NS_MSG_GENERATE_FAILURE(12525) +#define NS_ERROR_COMMUNICATIONS_ERROR NS_MSG_GENERATE_FAILURE(12526) +#define NS_ERROR_BUT_DONT_SHOW_ALERT NS_MSG_GENERATE_FAILURE(12527) +#define NS_ERROR_COULD_NOT_GET_USERS_MAIL_ADDRESS NS_MSG_GENERATE_FAILURE(12529) +#define NS_ERROR_COULD_NOT_GET_SENDERS_IDENTITY NS_MSG_GENERATE_FAILURE(12530) +#define NS_ERROR_MIME_MPART_ATTACHMENT_ERROR NS_MSG_GENERATE_FAILURE(12531) + +/* 12554 is taken by NS_ERROR_NNTP_NO_CROSS_POSTING. use 12555 as the next one */ + +// For message sending report +#define NS_MSG_ERROR_READING_FILE NS_MSG_GENERATE_FAILURE(12563) + +#define NS_MSG_ERROR_ATTACHING_FILE NS_MSG_GENERATE_FAILURE(12570) + +#define NS_ERROR_SMTP_GREETING NS_MSG_GENERATE_FAILURE(12572) + +#define NS_ERROR_SENDING_RCPT_COMMAND NS_MSG_GENERATE_FAILURE(12575) + +#define NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS NS_MSG_GENERATE_FAILURE(12582) + +#define NS_ERROR_SMTP_PASSWORD_UNDEFINED NS_MSG_GENERATE_FAILURE(12584) +#define NS_ERROR_SMTP_SEND_NOT_ALLOWED NS_MSG_GENERATE_FAILURE(12585) +#define NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED NS_MSG_GENERATE_FAILURE(12586) +#define NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2 NS_MSG_GENERATE_FAILURE(12588) + +#define NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER NS_MSG_GENERATE_FAILURE(12589) +#define NS_ERROR_SMTP_SEND_FAILED_REFUSED NS_MSG_GENERATE_FAILURE(12590) +#define NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED NS_MSG_GENERATE_FAILURE(12591) +#define NS_ERROR_SMTP_SEND_FAILED_TIMEOUT NS_MSG_GENERATE_FAILURE(12592) +#define NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_REASON NS_MSG_GENERATE_FAILURE(12593) + +#define NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL NS_MSG_GENERATE_FAILURE(12594) +#define NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL NS_MSG_GENERATE_FAILURE(12595) +#define NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT NS_MSG_GENERATE_FAILURE(12596) +#define NS_ERROR_SMTP_AUTH_FAILURE NS_MSG_GENERATE_FAILURE(12597) +#define NS_ERROR_SMTP_AUTH_GSSAPI NS_MSG_GENERATE_FAILURE(12598) +#define NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED NS_MSG_GENERATE_FAILURE(12599) + +#define NS_ERROR_ILLEGAL_LOCALPART NS_MSG_GENERATE_FAILURE(12601) + +#define NS_ERROR_CLIENTID NS_MSG_GENERATE_FAILURE(12610) +#define NS_ERROR_CLIENTID_PERMISSION NS_MSG_GENERATE_FAILURE(12611) + +const char* errorStringNameForErrorCode(nsresult aCode); + +#endif /* _nsComposeStrings_H__ */ + +// clang-format on diff --git a/comm/mailnews/compose/src/nsMsgAttachedFile.cpp b/comm/mailnews/compose/src/nsMsgAttachedFile.cpp new file mode 100644 index 0000000000..cb293ee964 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgAttachedFile.cpp @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsMsgAttachedFile.h" + +NS_IMPL_ISUPPORTS(nsMsgAttachedFile, nsIMsgAttachedFile) + +nsMsgAttachedFile::nsMsgAttachedFile() + : m_size(0), + m_unprintableCount(0), + m_highbitCount(0), + m_ctlCount(0), + m_nullCount(0), + m_maxLineLength(0) {} + +nsMsgAttachedFile::~nsMsgAttachedFile() {} + +NS_IMETHODIMP nsMsgAttachedFile::GetOrigUrl(nsIURI** aOrigUrl) { + NS_ENSURE_ARG_POINTER(aOrigUrl); + NS_IF_ADDREF(*aOrigUrl = m_origUrl); + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetOrigUrl(nsIURI* aOrigUrl) { + m_origUrl = aOrigUrl; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetTmpFile(nsIFile** aTmpFile) { + NS_ENSURE_ARG_POINTER(aTmpFile); + NS_IF_ADDREF(*aTmpFile = m_tmpFile); + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetTmpFile(nsIFile* aTmpFile) { + m_tmpFile = aTmpFile; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetType(nsACString& aType) { + aType = m_type; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetType(const nsACString& aType) { + m_type = aType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetEncoding(nsACString& aEncoding) { + aEncoding = m_encoding; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetEncoding(const nsACString& aEncoding) { + m_encoding = aEncoding; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetDescription(nsACString& aDescription) { + aDescription = m_description; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetDescription( + const nsACString& aDescription) { + m_description = aDescription; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetCloudPartInfo(nsACString& aCloudPartInfo) { + aCloudPartInfo = m_cloudPartInfo; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetCloudPartInfo( + const nsACString& aCloudPartInfo) { + m_cloudPartInfo = aCloudPartInfo; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetXMacType(nsACString& aXMacType) { + aXMacType = m_xMacType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetXMacType(const nsACString& aXMacType) { + m_xMacType = aXMacType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetXMacCreator(nsACString& aXMacCreator) { + aXMacCreator = m_xMacCreator; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetXMacCreator( + const nsACString& aXMacCreator) { + m_xMacCreator = aXMacCreator; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetRealName(nsACString& aRealName) { + aRealName = m_realName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetRealName(const nsACString& aRealName) { + m_realName = aRealName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetSize(uint32_t* aSize) { + NS_ENSURE_ARG_POINTER(aSize); + *aSize = m_size; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetSize(uint32_t aSize) { + m_size = aSize; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetUnprintableCount( + uint32_t* aUnprintableCount) { + NS_ENSURE_ARG_POINTER(aUnprintableCount); + *aUnprintableCount = m_unprintableCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetUnprintableCount( + uint32_t aUnprintableCount) { + m_unprintableCount = aUnprintableCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetHighbitCount(uint32_t* aHighbitCount) { + NS_ENSURE_ARG_POINTER(aHighbitCount); + *aHighbitCount = m_highbitCount; + return NS_OK; +} +NS_IMETHODIMP nsMsgAttachedFile::SetHighbitCount(uint32_t aHighbitCount) { + m_highbitCount = aHighbitCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetCtlCount(uint32_t* aCtlCount) { + NS_ENSURE_ARG_POINTER(aCtlCount); + *aCtlCount = m_ctlCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetCtlCount(uint32_t aCtlCount) { + m_ctlCount = aCtlCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetNullCount(uint32_t* aNullCount) { + NS_ENSURE_ARG_POINTER(aNullCount); + *aNullCount = m_nullCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetNullCount(uint32_t aNullCount) { + m_nullCount = aNullCount; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::GetMaxLineLength(uint32_t* aMaxLineLength) { + NS_ENSURE_ARG_POINTER(aMaxLineLength); + *aMaxLineLength = m_maxLineLength; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachedFile::SetMaxLineLength(uint32_t aMaxLineLength) { + m_maxLineLength = aMaxLineLength; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgAttachedFile.h b/comm/mailnews/compose/src/nsMsgAttachedFile.h new file mode 100644 index 0000000000..5f68731927 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgAttachedFile.h @@ -0,0 +1,13 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgAttachedFile_H_ +#define _nsMsgAttachedFile_H_ + +#include "nsCOMPtr.h" +#include "nsIMsgSend.h" +#include "nsMsgAttachmentData.h" + +#endif /* _nsMsgAttachedFile_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgAttachment.cpp b/comm/mailnews/compose/src/nsMsgAttachment.cpp new file mode 100644 index 0000000000..55edd348ca --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgAttachment.cpp @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgAttachment.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsMsgCompUtils.h" + +NS_IMPL_ISUPPORTS(nsMsgAttachment, nsIMsgAttachment) + +nsMsgAttachment::nsMsgAttachment() { + mTemporary = false; + mSendViaCloud = false; + mSize = -1; +} + +nsMsgAttachment::~nsMsgAttachment() { + MOZ_LOG(Compose, mozilla::LogLevel::Debug, ("~nsMsgAttachment()")); +} + +/* attribute wstring name; */ +NS_IMETHODIMP nsMsgAttachment::GetName(nsAString& aName) { + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetName(const nsAString& aName) { + mName = aName; + return NS_OK; +} + +/* attribute string url; */ +NS_IMETHODIMP nsMsgAttachment::GetUrl(nsACString& aUrl) { + aUrl = mUrl; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetUrl(const nsACString& aUrl) { + mUrl = aUrl; + return NS_OK; +} + +/* attribute string msgUri; */ +NS_IMETHODIMP nsMsgAttachment::GetMsgUri(nsACString& aMsgUri) { + aMsgUri = mMsgUri; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetMsgUri(const nsACString& aMsgUri) { + mMsgUri = aMsgUri; + return NS_OK; +} + +/* attribute string urlCharset; */ +NS_IMETHODIMP nsMsgAttachment::GetUrlCharset(nsACString& aUrlCharset) { + aUrlCharset = mUrlCharset; + return NS_OK; +} +NS_IMETHODIMP nsMsgAttachment::SetUrlCharset(const nsACString& aUrlCharset) { + mUrlCharset = aUrlCharset; + return NS_OK; +} + +/* attribute boolean temporary; */ +NS_IMETHODIMP nsMsgAttachment::GetTemporary(bool* aTemporary) { + NS_ENSURE_ARG_POINTER(aTemporary); + + *aTemporary = mTemporary; + return NS_OK; +} +NS_IMETHODIMP nsMsgAttachment::SetTemporary(bool aTemporary) { + mTemporary = aTemporary; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetSendViaCloud(bool* aSendViaCloud) { + NS_ENSURE_ARG_POINTER(aSendViaCloud); + + *aSendViaCloud = mSendViaCloud; + return NS_OK; +} +NS_IMETHODIMP nsMsgAttachment::SetSendViaCloud(bool aSendViaCloud) { + mSendViaCloud = aSendViaCloud; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetHtmlAnnotation( + const nsACString& aAnnotation) { + mHtmlAnnotation = aAnnotation; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetHtmlAnnotation(nsACString& aAnnotation) { + aAnnotation = mHtmlAnnotation; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAttachment::SetCloudFileAccountKey( + const nsACString& aCloudFileAccountKey) { + mCloudFileAccountKey = aCloudFileAccountKey; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAttachment::GetCloudFileAccountKey(nsACString& aCloudFileAccountKey) { + aCloudFileAccountKey = mCloudFileAccountKey; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetCloudPartHeaderData( + nsACString& aCloudPartHeaderData) { + aCloudPartHeaderData = mCloudPartHeaderData; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetCloudPartHeaderData( + const nsACString& aCloudPartHeaderData) { + mCloudPartHeaderData = aCloudPartHeaderData; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetContentLocation( + nsACString& aContentLocation) { + aContentLocation = mContentLocation; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetContentLocation( + const nsACString& aContentLocation) { + mContentLocation = aContentLocation; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetContentType(char** aContentType) { + NS_ENSURE_ARG_POINTER(aContentType); + + *aContentType = ToNewCString(mContentType); + return (*aContentType ? NS_OK : NS_ERROR_OUT_OF_MEMORY); +} + +NS_IMETHODIMP nsMsgAttachment::SetContentType(const char* aContentType) { + mContentType = aContentType; + // a full content type could also contains parameters but we need to + // keep only the content type alone. Therefore we need to cleanup it. + int32_t offset = mContentType.FindChar(';'); + if (offset >= 0) mContentType.SetLength(offset); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetContentTypeParam(char** aContentTypeParam) { + NS_ENSURE_ARG_POINTER(aContentTypeParam); + + *aContentTypeParam = ToNewCString(mContentTypeParam); + return (*aContentTypeParam ? NS_OK : NS_ERROR_OUT_OF_MEMORY); +} + +NS_IMETHODIMP nsMsgAttachment::SetContentTypeParam( + const char* aContentTypeParam) { + if (aContentTypeParam) + while (*aContentTypeParam == ';' || *aContentTypeParam == ' ') + aContentTypeParam++; + mContentTypeParam = aContentTypeParam; + + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::GetContentId(nsACString& aContentId) { + aContentId = mContentId; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachment::SetContentId(const nsACString& aContentId) { + mContentId = aContentId; + return NS_OK; +} + +/* attribute string charset; */ +NS_IMETHODIMP nsMsgAttachment::GetCharset(char** aCharset) { + NS_ENSURE_ARG_POINTER(aCharset); + + *aCharset = ToNewCString(mCharset); + return (*aCharset ? NS_OK : NS_ERROR_OUT_OF_MEMORY); +} +NS_IMETHODIMP nsMsgAttachment::SetCharset(const char* aCharset) { + mCharset = aCharset; + return NS_OK; +} + +/* attribute string macType; */ +NS_IMETHODIMP nsMsgAttachment::GetMacType(char** aMacType) { + NS_ENSURE_ARG_POINTER(aMacType); + + *aMacType = ToNewCString(mMacType); + return (*aMacType ? NS_OK : NS_ERROR_OUT_OF_MEMORY); +} +NS_IMETHODIMP nsMsgAttachment::SetMacType(const char* aMacType) { + mMacType = aMacType; + return NS_OK; +} + +/* attribute string macCreator; */ +NS_IMETHODIMP nsMsgAttachment::GetMacCreator(char** aMacCreator) { + NS_ENSURE_ARG_POINTER(aMacCreator); + + *aMacCreator = ToNewCString(mMacCreator); + return (*aMacCreator ? NS_OK : NS_ERROR_OUT_OF_MEMORY); +} +NS_IMETHODIMP nsMsgAttachment::SetMacCreator(const char* aMacCreator) { + mMacCreator = aMacCreator; + return NS_OK; +} + +/* attribute int64_t size; */ +NS_IMETHODIMP nsMsgAttachment::GetSize(int64_t* aSize) { + NS_ENSURE_ARG_POINTER(aSize); + + *aSize = mSize; + return NS_OK; +} +NS_IMETHODIMP nsMsgAttachment::SetSize(int64_t aSize) { + mSize = aSize; + return NS_OK; +} + +/* boolean equalsUrl (in nsIMsgAttachment attachment); */ +NS_IMETHODIMP nsMsgAttachment::EqualsUrl(nsIMsgAttachment* attachment, + bool* _retval) { + NS_ENSURE_ARG_POINTER(attachment); + NS_ENSURE_ARG_POINTER(_retval); + + nsAutoCString url; + attachment->GetUrl(url); + + *_retval = mUrl.Equals(url); + return NS_OK; +} + +nsresult nsMsgAttachment::DeleteAttachment() { + nsresult rv; + bool isAFile = false; + + nsCOMPtr<nsIFile> urlFile; + rv = NS_GetFileFromURLSpec(mUrl, getter_AddRefs(urlFile)); + NS_ASSERTION(NS_SUCCEEDED(rv), "Can't nsIFile from URL string"); + if (NS_SUCCEEDED(rv)) { + bool bExists = false; + rv = urlFile->Exists(&bExists); + NS_ASSERTION(NS_SUCCEEDED(rv), "Exists() call failed!"); + if (NS_SUCCEEDED(rv) && bExists) { + rv = urlFile->IsFile(&isAFile); + NS_ASSERTION(NS_SUCCEEDED(rv), "IsFile() call failed!"); + } + } + + // remove it if it's a valid file + if (isAFile) rv = urlFile->Remove(false); + + return rv; +} diff --git a/comm/mailnews/compose/src/nsMsgAttachment.h b/comm/mailnews/compose/src/nsMsgAttachment.h new file mode 100644 index 0000000000..d9e0de696e --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgAttachment.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgAttachment_H_ +#define _nsMsgAttachment_H_ + +#include "nsIMsgAttachment.h" +#include "nsString.h" + +class nsMsgAttachment : public nsIMsgAttachment { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGATTACHMENT + + nsMsgAttachment(); + + private: + virtual ~nsMsgAttachment(); + nsresult DeleteAttachment(); + + nsString mName; + nsCString mUrl; + nsCString mMsgUri; + nsCString mUrlCharset; + bool mTemporary; + bool mSendViaCloud; + nsCString mCloudFileAccountKey; + nsCString mCloudPartHeaderData; + nsCString mContentLocation; + nsCString mContentType; + nsCString mContentTypeParam; + nsCString mContentId; + nsCString mCharset; + nsCString mMacType; + nsCString mMacCreator; + nsCString mHtmlAnnotation; + int64_t mSize; +}; + +#endif /* _nsMsgAttachment_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgAttachmentData.cpp b/comm/mailnews/compose/src/nsMsgAttachmentData.cpp new file mode 100644 index 0000000000..c32b31d0c1 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgAttachmentData.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsMsgAttachmentData.h" + +NS_IMPL_ISUPPORTS(nsMsgAttachmentData, nsIMsgAttachmentData) + +nsMsgAttachmentData::nsMsgAttachmentData() + : m_size(0), + m_sizeExternalStr("-1"), + m_isExternalAttachment(false), + m_isExternalLinkAttachment(false), + m_isDownloaded(false), + m_hasFilename(false), + m_displayableInline(false) {} + +nsMsgAttachmentData::~nsMsgAttachmentData() {} + +NS_IMETHODIMP nsMsgAttachmentData::GetUrl(nsIURI** aUrl) { + NS_ENSURE_ARG_POINTER(aUrl); + NS_IF_ADDREF(*aUrl = m_url); + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetUrl(nsIURI* aUrl) { + m_url = aUrl; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetDesiredType(nsACString& aDesiredType) { + aDesiredType = m_desiredType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetDesiredType( + const nsACString& aDesiredType) { + m_desiredType = aDesiredType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetRealType(nsACString& aRealType) { + aRealType = m_realType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetRealType(const nsACString& aRealType) { + m_realType = aRealType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetRealEncoding(nsACString& aRealEncoding) { + aRealEncoding = m_realEncoding; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetRealEncoding( + const nsACString& aRealEncoding) { + m_realEncoding = aRealEncoding; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetRealName(nsACString& aRealName) { + aRealName = m_realName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetRealName(const nsACString& aRealName) { + m_realName = aRealName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetDescription(nsACString& aDescription) { + aDescription = m_description; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetDescription( + const nsACString& aDescription) { + m_description = aDescription; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetXMacType(nsACString& aXMacType) { + aXMacType = m_xMacType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetXMacType(const nsACString& aXMacType) { + m_xMacType = aXMacType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::GetXMacCreator(nsACString& aXMacCreator) { + aXMacCreator = m_xMacCreator; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAttachmentData::SetXMacCreator( + const nsACString& aXMacCreator) { + m_xMacCreator = aXMacCreator; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgAttachmentData.h b/comm/mailnews/compose/src/nsMsgAttachmentData.h new file mode 100644 index 0000000000..763a4377e9 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgAttachmentData.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef __MSGATTACHMENTDATA_H__ +#define __MSGATTACHMENTDATA_H__ + +#include "nsIURL.h" +#include "nsString.h" +#include "nsIMsgSend.h" + +// Attachment file/URL structures - we're letting libmime use this directly +class nsMsgAttachmentData final : public nsIMsgAttachmentData { + public: + NS_DECL_NSIMSGATTACHMENTDATA + NS_DECL_ISUPPORTS + + nsMsgAttachmentData(); + virtual ~nsMsgAttachmentData(); + + nsCOMPtr<nsIURI> m_url; // The URL to attach. + + nsCString m_desiredType; // The type to which this document should be + // converted. Legal values are NULL, TEXT_PLAIN + // and APPLICATION_POSTSCRIPT (which are macros + // defined in net.h); other values are ignored. + + nsCString + m_realType; // The type of the URL if known, otherwise NULL. For example, + // if you were attaching a temp file which was known to + // contain HTML data, you would pass in TEXT_HTML as the + // real_type, to override whatever type the name of the tmp + // file might otherwise indicate. + + nsCString m_realEncoding; // Goes along with real_type + + nsCString + m_realName; // The original name of this document, which will eventually + // show up in the Content-Disposition header. For example, if + // you had copied a document to a tmp file, this would be the + // original, human-readable name of the document. + + nsCString m_description; // If you put a string here, it will show up as the + // Content-Description header. This can be any + // explanatory text; it's not a file name. + + nsCString m_disposition; // The Content-Disposition header (if any). a + // nsMsgAttachmentData can very well have + // Content-Disposition: inline value, instead of + // "attachment". + nsCString m_cloudPartInfo; // For X-Mozilla-Cloud-Part header, if any + + // Mac-specific data that should show up as optional parameters + // to the content-type header. + nsCString m_xMacType; + nsCString m_xMacCreator; + + int32_t m_size; // The size of the attachment. May be 0. + nsCString + m_sizeExternalStr; // The reported size of an external attachment. + // Originally set at "-1" to mean an unknown value. + bool m_isExternalAttachment; // Flag for determining if the attachment is + // external + bool m_isExternalLinkAttachment; // Flag for determining if the attachment is + // external and an http link. + bool m_isDownloaded; // Flag for determining if the attachment has already + // been downloaded + bool m_hasFilename; // Tells whether the name is provided by us or if it's a + // Part 1.2-like attachment + bool m_displayableInline; // Tells whether the attachment could be displayed + // inline +}; + +class nsMsgAttachedFile final : public nsIMsgAttachedFile { + public: + NS_DECL_NSIMSGATTACHEDFILE + NS_DECL_ISUPPORTS + + nsMsgAttachedFile(); + virtual ~nsMsgAttachedFile(); + + nsCOMPtr<nsIURI> m_origUrl; // Where it came from on the network (or even + // elsewhere on the local disk.) + + nsCOMPtr<nsIFile> m_tmpFile; // The tmp file in which the (possibly + // converted) data now resides. + + nsCString m_type; // The type of the data in file_name (not necessarily the + // same as the type of orig_url.) + + nsCString + m_encoding; // Likewise, the encoding of the tmp file. This will be set + // only if the original document had an encoding already; we + // don't do base64 encoding and so forth until it's time to + // assemble a full MIME message of all parts. + + nsCString m_description; // For Content-Description header + nsCString m_cloudPartInfo; // For X-Mozilla-Cloud-Part header, if any + nsCString m_xMacType; // mac-specific info + nsCString m_xMacCreator; // mac-specific info + nsCString m_realName; // The real name of the file. + + // Some statistics about the data that was written to the file, so that when + // it comes time to compose a MIME message, we can make an informed decision + // about what Content-Transfer-Encoding would be best for this attachment. + // (If it's encoded already, we ignore this information and ship it as-is.) + uint32_t m_size; + uint32_t m_unprintableCount; + uint32_t m_highbitCount; + uint32_t m_ctlCount; + uint32_t m_nullCount; + uint32_t m_maxLineLength; +}; + +#undef MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING +#ifdef MOZ_IS_DESTRUCTIBLE +# define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X) \ + static_assert( \ + !MOZ_IS_DESTRUCTIBLE(X) || \ + mozilla::IsSame<X, nsMsgAttachmentData>::value || \ + mozilla::IsSame<X, nsMsgAttachedFile>::value, \ + "Reference-counted class " #X \ + " should not have a public destructor. " \ + "Try to make this class's destructor non-public. If that is really " \ + "not possible, you can whitelist this class by providing a " \ + "HasDangerousPublicDestructor specialization for it."); +#else +# define MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(X) +#endif +#endif diff --git a/comm/mailnews/compose/src/nsMsgCompFields.cpp b/comm/mailnews/compose/src/nsMsgCompFields.cpp new file mode 100644 index 0000000000..9bfb66a253 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCompFields.cpp @@ -0,0 +1,559 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgCompose.h" +#include "nsMsgCompFields.h" +#include "nsMsgI18N.h" +#include "nsMsgCompUtils.h" +#include "nsMsgUtils.h" +#include "prmem.h" +#include "nsIFileChannel.h" +#include "nsIMsgAttachment.h" +#include "nsIMsgMdnGenerator.h" +#include "nsServiceManagerUtils.h" +#include "nsMemory.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/mailnews/MimeHeaderParser.h" + +using namespace mozilla::mailnews; + +struct HeaderInfo { + /// Header name + const char* mName; + /// If true, nsMsgCompFields should reflect the raw header value instead of + /// the unstructured header value. + bool mStructured; +}; + +// This is a mapping of the m_headers local set to the actual header name we +// store on the structured header object. +static HeaderInfo kHeaders[] = { + {"From", true}, + {"Reply-To", true}, + {"To", true}, + {"Cc", true}, + {"Bcc", true}, + {nullptr, false}, // FCC + {nullptr, false}, // FCC2 + {"Newsgroups", true}, + {"Followup-To", true}, + {"Subject", false}, + {"Organization", false}, + {"References", true}, + {"X-Mozilla-News-Host", false}, + {"X-Priority", false}, + {nullptr, false}, // CHARACTER_SET + {"Message-Id", true}, + {"X-Template", true}, + {nullptr, false}, // DRAFT_ID + {nullptr, false}, // TEMPLATE_ID + {"Content-Language", true}, + {nullptr, false} // CREATOR IDENTITY KEY +}; + +static_assert( + MOZ_ARRAY_LENGTH(kHeaders) == nsMsgCompFields::MSG_MAX_HEADERS, + "These two arrays need to be kept in sync or bad things will happen!"); + +NS_IMPL_ISUPPORTS(nsMsgCompFields, nsIMsgCompFields, msgIStructuredHeaders, + msgIWritableStructuredHeaders) + +nsMsgCompFields::nsMsgCompFields() + : mStructuredHeaders(do_CreateInstance(NS_ISTRUCTUREDHEADERS_CONTRACTID)) { + m_body.Truncate(); + + m_attachVCard = false; + m_forcePlainText = false; + m_useMultipartAlternative = false; + m_returnReceipt = false; + m_receiptHeaderType = nsIMsgMdnGenerator::eDntType; + m_DSN = false; + m_bodyIsAsciiOnly = false; + m_forceMsgEncoding = false; + m_needToCheckCharset = true; + m_attachmentReminder = false; + m_deliveryFormat = nsIMsgCompSendFormat::Unset; +} + +nsMsgCompFields::~nsMsgCompFields() { + MOZ_LOG(Compose, mozilla::LogLevel::Debug, ("~nsMsgCompFields()")); +} + +nsresult nsMsgCompFields::SetAsciiHeader(MsgHeaderID header, + const char* value) { + NS_ASSERTION(header >= 0 && header < MSG_MAX_HEADERS, + "Invalid message header index!"); + + // If we are storing this on the structured header object, we need to set the + // value on that object as well. Note that the value may be null, which we'll + // take as an attempt to delete the header. + const char* headerName = kHeaders[header].mName; + if (headerName) { + if (!value || !*value) return mStructuredHeaders->DeleteHeader(headerName); + + return mStructuredHeaders->SetRawHeader(headerName, + nsDependentCString(value)); + } + + // Not on the structurd header object, so save it locally. + m_headers[header] = value; + + return NS_OK; +} + +const char* nsMsgCompFields::GetAsciiHeader(MsgHeaderID header) { + NS_ASSERTION(header >= 0 && header < MSG_MAX_HEADERS, + "Invalid message header index!"); + + const char* headerName = kHeaders[header].mName; + if (headerName) { + // We may be out of sync with the structured header object. Retrieve the + // header value. + if (kHeaders[header].mStructured) { + mStructuredHeaders->GetRawHeader(headerName, m_headers[header]); + } else { + nsString value; + mStructuredHeaders->GetUnstructuredHeader(headerName, value); + CopyUTF16toUTF8(value, m_headers[header]); + } + } + + return m_headers[header].get(); +} + +nsresult nsMsgCompFields::SetUnicodeHeader(MsgHeaderID header, + const nsAString& value) { + return SetAsciiHeader(header, NS_ConvertUTF16toUTF8(value).get()); +} + +nsresult nsMsgCompFields::GetUnicodeHeader(MsgHeaderID header, + nsAString& aResult) { + CopyUTF8toUTF16(nsDependentCString(GetAsciiHeader(header)), aResult); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetFrom(const nsAString& value) { + return SetUnicodeHeader(MSG_FROM_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetFrom(nsAString& _retval) { + return GetUnicodeHeader(MSG_FROM_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetReplyTo(const nsAString& value) { + return SetUnicodeHeader(MSG_REPLY_TO_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetReplyTo(nsAString& _retval) { + return GetUnicodeHeader(MSG_REPLY_TO_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetTo(const nsAString& value) { + return SetUnicodeHeader(MSG_TO_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetTo(nsAString& _retval) { + return GetUnicodeHeader(MSG_TO_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetCc(const nsAString& value) { + return SetUnicodeHeader(MSG_CC_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetCc(nsAString& _retval) { + return GetUnicodeHeader(MSG_CC_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetBcc(const nsAString& value) { + return SetUnicodeHeader(MSG_BCC_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetBcc(nsAString& _retval) { + return GetUnicodeHeader(MSG_BCC_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetFcc(const nsAString& value) { + return SetUnicodeHeader(MSG_FCC_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetFcc(nsAString& _retval) { + return GetUnicodeHeader(MSG_FCC_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetFcc2(const nsAString& value) { + return SetUnicodeHeader(MSG_FCC2_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetFcc2(nsAString& _retval) { + return GetUnicodeHeader(MSG_FCC2_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetNewsgroups(const nsAString& aValue) { + return SetUnicodeHeader(MSG_NEWSGROUPS_HEADER_ID, aValue); +} + +NS_IMETHODIMP nsMsgCompFields::GetNewsgroups(nsAString& aGroup) { + return GetUnicodeHeader(MSG_NEWSGROUPS_HEADER_ID, aGroup); +} + +NS_IMETHODIMP nsMsgCompFields::SetFollowupTo(const nsAString& aValue) { + return SetUnicodeHeader(MSG_FOLLOWUP_TO_HEADER_ID, aValue); +} + +NS_IMETHODIMP nsMsgCompFields::GetFollowupTo(nsAString& _retval) { + return GetUnicodeHeader(MSG_FOLLOWUP_TO_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::GetHasRecipients(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + *_retval = NS_SUCCEEDED(mime_sanity_check_fields_recipients( + GetTo(), GetCc(), GetBcc(), GetNewsgroups())); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetCreatorIdentityKey(const char* value) { + return SetAsciiHeader(MSG_CREATOR_IDENTITY_KEY_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetCreatorIdentityKey(char** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = strdup(GetAsciiHeader(MSG_CREATOR_IDENTITY_KEY_ID)); + return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgCompFields::SetSubject(const nsAString& value) { + return SetUnicodeHeader(MSG_SUBJECT_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetSubject(nsAString& _retval) { + return GetUnicodeHeader(MSG_SUBJECT_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetOrganization(const nsAString& value) { + return SetUnicodeHeader(MSG_ORGANIZATION_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetOrganization(nsAString& _retval) { + return GetUnicodeHeader(MSG_ORGANIZATION_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetReferences(const char* value) { + return SetAsciiHeader(MSG_REFERENCES_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetReferences(char** _retval) { + *_retval = strdup(GetAsciiHeader(MSG_REFERENCES_HEADER_ID)); + return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgCompFields::SetNewspostUrl(const char* value) { + return SetAsciiHeader(MSG_NEWSPOSTURL_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetNewspostUrl(char** _retval) { + *_retval = strdup(GetAsciiHeader(MSG_NEWSPOSTURL_HEADER_ID)); + return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgCompFields::SetPriority(const char* value) { + return SetAsciiHeader(MSG_PRIORITY_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetPriority(char** _retval) { + *_retval = strdup(GetAsciiHeader(MSG_PRIORITY_HEADER_ID)); + return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgCompFields::SetMessageId(const char* value) { + return SetAsciiHeader(MSG_MESSAGE_ID_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetMessageId(char** _retval) { + *_retval = strdup(GetAsciiHeader(MSG_MESSAGE_ID_HEADER_ID)); + return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgCompFields::SetTemplateName(const nsAString& value) { + return SetUnicodeHeader(MSG_X_TEMPLATE_HEADER_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetTemplateName(nsAString& _retval) { + return GetUnicodeHeader(MSG_X_TEMPLATE_HEADER_ID, _retval); +} + +NS_IMETHODIMP nsMsgCompFields::SetDraftId(const nsACString& value) { + return SetAsciiHeader(MSG_DRAFT_ID_HEADER_ID, + PromiseFlatCString(value).get()); +} + +NS_IMETHODIMP nsMsgCompFields::GetDraftId(nsACString& _retval) { + _retval.Assign(GetAsciiHeader(MSG_DRAFT_ID_HEADER_ID)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetTemplateId(const nsACString& value) { + return SetAsciiHeader(MSG_TEMPLATE_ID_HEADER_ID, + PromiseFlatCString(value).get()); +} + +NS_IMETHODIMP nsMsgCompFields::GetTemplateId(nsACString& _retval) { + _retval.Assign(GetAsciiHeader(MSG_TEMPLATE_ID_HEADER_ID)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetReturnReceipt(bool value) { + m_returnReceipt = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetReturnReceipt(bool* _retval) { + *_retval = m_returnReceipt; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetReceiptHeaderType(int32_t value) { + m_receiptHeaderType = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetReceiptHeaderType(int32_t* _retval) { + *_retval = m_receiptHeaderType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetDSN(bool value) { + m_DSN = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetDSN(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = m_DSN; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetAttachVCard(bool value) { + m_attachVCard = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetAttachVCard(bool* _retval) { + *_retval = m_attachVCard; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetAttachmentReminder(bool* _retval) { + *_retval = m_attachmentReminder; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetAttachmentReminder(bool value) { + m_attachmentReminder = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetDeliveryFormat(int32_t value) { + switch (value) { + case nsIMsgCompSendFormat::Auto: + case nsIMsgCompSendFormat::PlainText: + case nsIMsgCompSendFormat::HTML: + case nsIMsgCompSendFormat::Both: + m_deliveryFormat = value; + break; + case nsIMsgCompSendFormat::Unset: + default: + m_deliveryFormat = nsIMsgCompSendFormat::Unset; + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetDeliveryFormat(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = m_deliveryFormat; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetContentLanguage(const char* value) { + return SetAsciiHeader(MSG_CONTENT_LANGUAGE_ID, value); +} + +NS_IMETHODIMP nsMsgCompFields::GetContentLanguage(char** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = strdup(GetAsciiHeader(MSG_CONTENT_LANGUAGE_ID)); + return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgCompFields::SetForcePlainText(bool value) { + m_forcePlainText = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetForcePlainText(bool* _retval) { + *_retval = m_forcePlainText; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetForceMsgEncoding(bool value) { + m_forceMsgEncoding = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetForceMsgEncoding(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = m_forceMsgEncoding; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetUseMultipartAlternative(bool value) { + m_useMultipartAlternative = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetUseMultipartAlternative(bool* _retval) { + *_retval = m_useMultipartAlternative; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetBodyIsAsciiOnly(bool value) { + m_bodyIsAsciiOnly = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetBodyIsAsciiOnly(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + *_retval = m_bodyIsAsciiOnly; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetBody(const nsAString& value) { + m_body = value; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetBody(nsAString& _retval) { + _retval = m_body; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetAttachments( + nsTArray<RefPtr<nsIMsgAttachment>>& attachments) { + attachments = m_attachments.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::AddAttachment(nsIMsgAttachment* attachment) { + // Don't add the same attachment twice. + for (nsIMsgAttachment* a : m_attachments) { + bool sameUrl; + a->EqualsUrl(attachment, &sameUrl); + if (sameUrl) return NS_OK; + } + m_attachments.AppendElement(attachment); + return NS_OK; +} + +/* void removeAttachment (in nsIMsgAttachment attachment); */ +NS_IMETHODIMP nsMsgCompFields::RemoveAttachment(nsIMsgAttachment* attachment) { + for (uint32_t i = 0; i < m_attachments.Length(); i++) { + bool sameUrl; + m_attachments[i]->EqualsUrl(attachment, &sameUrl); + if (sameUrl) { + m_attachments.RemoveElementAt(i); + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetOtherHeaders( + const nsTArray<nsString>& headers) { + m_otherHeaders = headers.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetOtherHeaders(nsTArray<nsString>& headers) { + headers = m_otherHeaders.Clone(); + return NS_OK; +} + +/* void removeAttachments (); */ +NS_IMETHODIMP nsMsgCompFields::RemoveAttachments() { + m_attachments.Clear(); + return NS_OK; +} + +// This method is called during the creation of a new window. +NS_IMETHODIMP +nsMsgCompFields::SplitRecipients(const nsAString& aRecipients, + bool aEmailAddressOnly, + nsTArray<nsString>& aResult) { + nsCOMArray<msgIAddressObject> header(EncodedHeaderW(aRecipients)); + if (aEmailAddressOnly) + ExtractEmails(header, aResult); + else + ExtractDisplayAddresses(header, aResult); + + return NS_OK; +} + +// This method is called during the sending of message from +// nsMsgCompose::CheckAndPopulateRecipients() +nsresult nsMsgCompFields::SplitRecipientsEx(const nsAString& recipients, + nsTArray<nsMsgRecipient>& aResult) { + nsTArray<nsString> names, addresses; + ExtractAllAddresses(EncodedHeaderW(recipients), names, addresses); + + uint32_t numAddresses = names.Length(); + for (uint32_t i = 0; i < numAddresses; ++i) { + nsMsgRecipient msgRecipient; + msgRecipient.mEmail = addresses[i]; + msgRecipient.mName = names[i]; + aResult.AppendElement(msgRecipient); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::ConvertBodyToPlainText() { + nsresult rv = NS_OK; + + if (!m_body.IsEmpty()) { + if (NS_SUCCEEDED(rv)) { + bool flowed, formatted; + GetSerialiserFlags(&flowed, &formatted); + rv = ConvertBufToPlainText(m_body, flowed, formatted, true); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgCompFields::GetComposeSecure( + nsIMsgComposeSecure** aComposeSecure) { + NS_ENSURE_ARG_POINTER(aComposeSecure); + NS_IF_ADDREF(*aComposeSecure = mSecureCompFields); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetComposeSecure( + nsIMsgComposeSecure* aComposeSecure) { + mSecureCompFields = aComposeSecure; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::GetNeedToCheckCharset(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = m_needToCheckCharset; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompFields::SetNeedToCheckCharset(bool aCheck) { + m_needToCheckCharset = aCheck; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgCompFields.h b/comm/mailnews/compose/src/nsMsgCompFields.h new file mode 100644 index 0000000000..312d19192c --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCompFields.h @@ -0,0 +1,212 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _MsgCompFields_H_ +#define _MsgCompFields_H_ + +#include "nsIMsgCompFields.h" +#include "msgCore.h" +#include "nsIAbCard.h" +#include "nsIAbDirectory.h" +#include "nsTArray.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIMsgComposeSecure.h" + +struct nsMsgRecipient { + nsString mName; + nsString mEmail; + nsCOMPtr<nsIAbCard> mCard; + nsCOMPtr<nsIAbDirectory> mDirectory; +}; + +/* Note that all the "Get" methods never return NULL (except in case of serious + error, like an illegal parameter); rather, they return "" if things were set + to NULL. This makes it real handy for the callers. */ + +class nsMsgCompFields : public nsIMsgCompFields { + public: + nsMsgCompFields(); + + /* this macro defines QueryInterface, AddRef and Release for this class */ + NS_DECL_THREADSAFE_ISUPPORTS + NS_FORWARD_MSGISTRUCTUREDHEADERS(mStructuredHeaders->) + NS_FORWARD_MSGIWRITABLESTRUCTUREDHEADERS(mStructuredHeaders->) + NS_DECL_NSIMSGCOMPFIELDS + + typedef enum MsgHeaderID { + MSG_FROM_HEADER_ID = 0, + MSG_REPLY_TO_HEADER_ID, + MSG_TO_HEADER_ID, + MSG_CC_HEADER_ID, + MSG_BCC_HEADER_ID, + MSG_FCC_HEADER_ID, + MSG_FCC2_HEADER_ID, + MSG_NEWSGROUPS_HEADER_ID, + MSG_FOLLOWUP_TO_HEADER_ID, + MSG_SUBJECT_HEADER_ID, + MSG_ORGANIZATION_HEADER_ID, + MSG_REFERENCES_HEADER_ID, + MSG_NEWSPOSTURL_HEADER_ID, + MSG_PRIORITY_HEADER_ID, + MSG_CHARACTER_SET_HEADER_ID, + MSG_MESSAGE_ID_HEADER_ID, + MSG_X_TEMPLATE_HEADER_ID, + MSG_DRAFT_ID_HEADER_ID, + MSG_TEMPLATE_ID_HEADER_ID, + MSG_CONTENT_LANGUAGE_ID, + MSG_CREATOR_IDENTITY_KEY_ID, + + MSG_MAX_HEADERS // Must be the last one. + } MsgHeaderID; + + nsresult SetAsciiHeader(MsgHeaderID header, const char* value); + const char* GetAsciiHeader( + MsgHeaderID header); // just return the address of the internal header + // variable, don't dispose it + + nsresult SetUnicodeHeader(MsgHeaderID header, const nsAString& value); + nsresult GetUnicodeHeader(MsgHeaderID header, nsAString& _retval); + + /* Convenience routines to get and set header's value... + + IMPORTANT: + all routines const char* GetXxx(void) will return a pointer to the header, + please don't free it. + */ + + nsresult SetFrom(const char* value) { + return SetAsciiHeader(MSG_FROM_HEADER_ID, value); + } + const char* GetFrom(void) { return GetAsciiHeader(MSG_FROM_HEADER_ID); } + + nsresult SetReplyTo(const char* value) { + return SetAsciiHeader(MSG_REPLY_TO_HEADER_ID, value); + } + const char* GetReplyTo() { return GetAsciiHeader(MSG_REPLY_TO_HEADER_ID); } + + nsresult SetTo(const char* value) { + return SetAsciiHeader(MSG_TO_HEADER_ID, value); + } + const char* GetTo() { return GetAsciiHeader(MSG_TO_HEADER_ID); } + + nsresult SetCc(const char* value) { + return SetAsciiHeader(MSG_CC_HEADER_ID, value); + } + const char* GetCc() { return GetAsciiHeader(MSG_CC_HEADER_ID); } + + nsresult SetBcc(const char* value) { + return SetAsciiHeader(MSG_BCC_HEADER_ID, value); + } + const char* GetBcc() { return GetAsciiHeader(MSG_BCC_HEADER_ID); } + + nsresult SetFcc(const char* value) { + return SetAsciiHeader(MSG_FCC_HEADER_ID, value); + } + const char* GetFcc() { return GetAsciiHeader(MSG_FCC_HEADER_ID); } + + nsresult SetFcc2(const char* value) { + return SetAsciiHeader(MSG_FCC2_HEADER_ID, value); + } + const char* GetFcc2() { return GetAsciiHeader(MSG_FCC2_HEADER_ID); } + + nsresult SetNewsgroups(const char* aValue) { + return SetAsciiHeader(MSG_NEWSGROUPS_HEADER_ID, aValue); + } + const char* GetNewsgroups() { + return GetAsciiHeader(MSG_NEWSGROUPS_HEADER_ID); + } + + nsresult SetFollowupTo(const char* aValue) { + return SetAsciiHeader(MSG_FOLLOWUP_TO_HEADER_ID, aValue); + } + const char* GetFollowupTo() { + return GetAsciiHeader(MSG_FOLLOWUP_TO_HEADER_ID); + } + + nsresult SetSubject(const char* value) { + return SetAsciiHeader(MSG_SUBJECT_HEADER_ID, value); + } + const char* GetSubject() { return GetAsciiHeader(MSG_SUBJECT_HEADER_ID); } + + nsresult SetOrganization(const char* value) { + return SetAsciiHeader(MSG_ORGANIZATION_HEADER_ID, value); + } + const char* GetOrganization() { + return GetAsciiHeader(MSG_ORGANIZATION_HEADER_ID); + } + + const char* GetReferences() { + return GetAsciiHeader(MSG_REFERENCES_HEADER_ID); + } + + const char* GetNewspostUrl() { + return GetAsciiHeader(MSG_NEWSPOSTURL_HEADER_ID); + } + + const char* GetPriority() { return GetAsciiHeader(MSG_PRIORITY_HEADER_ID); } + + const char* GetCharacterSet() { + return GetAsciiHeader(MSG_CHARACTER_SET_HEADER_ID); + } + + const char* GetMessageId() { + return GetAsciiHeader(MSG_MESSAGE_ID_HEADER_ID); + } + + nsresult SetTemplateName(const char* value) { + return SetAsciiHeader(MSG_X_TEMPLATE_HEADER_ID, value); + } + const char* GetTemplateName() { + return GetAsciiHeader(MSG_X_TEMPLATE_HEADER_ID); + } + + const char* GetDraftId() { return GetAsciiHeader(MSG_DRAFT_ID_HEADER_ID); } + const char* GetTemplateId() { + return GetAsciiHeader(MSG_TEMPLATE_ID_HEADER_ID); + } + + const char* GetContentLanguage() { + return GetAsciiHeader(MSG_CONTENT_LANGUAGE_ID); + } + + bool GetReturnReceipt() { return m_returnReceipt; } + bool GetDSN() { return m_DSN; } + bool GetAttachVCard() { return m_attachVCard; } + bool GetAttachmentReminder() { return m_attachmentReminder; } + int32_t GetDeliveryFormat() { return m_deliveryFormat; } + bool GetForcePlainText() { return m_forcePlainText; } + bool GetUseMultipartAlternative() { return m_useMultipartAlternative; } + bool GetBodyIsAsciiOnly() { return m_bodyIsAsciiOnly; } + bool GetForceMsgEncoding() { return m_forceMsgEncoding; } + + nsresult SplitRecipientsEx(const nsAString& recipients, + nsTArray<nsMsgRecipient>& aResult); + + protected: + virtual ~nsMsgCompFields(); + nsCString m_headers[MSG_MAX_HEADERS]; + nsString m_body; + nsTArray<RefPtr<nsIMsgAttachment>> m_attachments; + nsTArray<nsString> m_otherHeaders; + bool m_attachVCard; + bool m_attachmentReminder; + int32_t m_deliveryFormat; + bool m_forcePlainText; + bool m_useMultipartAlternative; + bool m_returnReceipt; + bool m_DSN; + bool m_bodyIsAsciiOnly; + bool m_forceMsgEncoding; + int32_t m_receiptHeaderType; /* receipt header type */ + nsCString m_DefaultCharacterSet; + bool m_needToCheckCharset; + + nsCOMPtr<nsIMsgComposeSecure> mSecureCompFields; + nsCOMPtr<msgIWritableStructuredHeaders> mStructuredHeaders; +}; + +#endif /* _MsgCompFields_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgCompUtils.cpp b/comm/mailnews/compose/src/nsMsgCompUtils.cpp new file mode 100644 index 0000000000..50ce86497b --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCompUtils.cpp @@ -0,0 +1,1164 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsCOMPtr.h" +#include "nsMsgCompUtils.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsStringFwd.h" +#include "prmem.h" +#include "nsIStringBundle.h" +#include "nsIIOService.h" +#include "nsIHttpProtocolHandler.h" +#include "nsMailHeaders.h" +#include "nsMsgI18N.h" +#include "nsINntpService.h" +#include "nsMimeTypes.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsMsgPrompts.h" +#include "nsMsgUtils.h" +#include "nsCExternalHandlerService.h" +#include "nsIMIMEService.h" +#include "nsComposeStrings.h" +#include "nsIMsgCompUtils.h" +#include "nsIMsgMdnGenerator.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsMemory.h" +#include "nsCRTGlue.h" +#include <ctype.h> +#include "mozilla/dom/Element.h" +#include "mozilla/EncodingDetector.h" +#include "mozilla/Components.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/ContentIterator.h" +#include "mozilla/dom/Document.h" +#include "nsIMIMEInfo.h" +#include "nsIMsgHeaderParser.h" +#include "nsIMutableArray.h" +#include "nsIRandomGenerator.h" +#include "nsID.h" + +void msg_generate_message_id(nsIMsgIdentity* identity, + const nsACString& customHost, + nsACString& messageID); + +NS_IMPL_ISUPPORTS(nsMsgCompUtils, nsIMsgCompUtils) + +nsMsgCompUtils::nsMsgCompUtils() {} + +nsMsgCompUtils::~nsMsgCompUtils() {} + +NS_IMETHODIMP nsMsgCompUtils::MimeMakeSeparator(const char* prefix, + char** _retval) { + NS_ENSURE_ARG_POINTER(prefix); + NS_ENSURE_ARG_POINTER(_retval); + *_retval = mime_make_separator(prefix); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompUtils::MsgGenerateMessageId(nsIMsgIdentity* identity, + const nsACString& host, + nsACString& messageID) { + // We don't check `host` because it's allowed to be a null pointer (which + // means we should ignore it for message ID generation). + NS_ENSURE_ARG_POINTER(identity); + msg_generate_message_id(identity, host, messageID); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompUtils::GetMsgMimeConformToStandard(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nsMsgMIMEGetConformToStandard(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompUtils::DetectCharset(const nsACString& aContent, + nsACString& aCharset) { + mozilla::UniquePtr<mozilla::EncodingDetector> detector = + mozilla::EncodingDetector::Create(); + mozilla::Span<const uint8_t> src = mozilla::AsBytes( + mozilla::Span(ToNewCString(aContent), aContent.Length())); + mozilla::Unused << detector->Feed(src, true); + auto encoding = detector->Guess(nullptr, true); + encoding->Name(aCharset); + return NS_OK; +} + +// +// Create a file for the a unique temp file +// on the local machine. Caller must free memory +// +nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile) { + if ((!tFileName) || (!*tFileName)) tFileName = "nsmail.tmp"; + + nsresult rv = + GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, tFileName, tFile); + + NS_ENSURE_SUCCESS(rv, rv); + + rv = (*tFile)->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + if (NS_FAILED(rv)) NS_RELEASE(*tFile); + + return rv; +} + +// This is the value a caller will Get if they don't Set first (like MDN +// sending a return receipt), so init to the default value of the +// mail.strictly_mime_headers preference. +static bool mime_headers_use_quoted_printable_p = true; + +bool nsMsgMIMEGetConformToStandard(void) { + return mime_headers_use_quoted_printable_p; +} + +void nsMsgMIMESetConformToStandard(bool conform_p) { + /* + * If we are conforming to mime standard no matter what we set + * for the headers preference when generating mime headers we should + * also conform to the standard. Otherwise, depends the preference + * we set. For now, the headers preference is not accessible from UI. + */ + if (conform_p) + mime_headers_use_quoted_printable_p = true; + else { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefs( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + prefs->GetBoolPref("mail.strictly_mime_headers", + &mime_headers_use_quoted_printable_p); + } + } +} + +/** + * Checks if the recipient fields have sane values for message send. + */ +nsresult mime_sanity_check_fields_recipients(const char* to, const char* cc, + const char* bcc, + const char* newsgroups) { + if (to) + while (IS_SPACE(*to)) to++; + if (cc) + while (IS_SPACE(*cc)) cc++; + if (bcc) + while (IS_SPACE(*bcc)) bcc++; + if (newsgroups) + while (IS_SPACE(*newsgroups)) newsgroups++; + + if ((!to || !*to) && (!cc || !*cc) && (!bcc || !*bcc) && + (!newsgroups || !*newsgroups)) + return NS_MSG_NO_RECIPIENTS; + + return NS_OK; +} + +/** + * Checks if the compose fields have sane values for message send. + */ +nsresult mime_sanity_check_fields( + const char* from, const char* reply_to, const char* to, const char* cc, + const char* bcc, const char* fcc, const char* newsgroups, + const char* followup_to, const char* /*subject*/, + const char* /*references*/, const char* /*organization*/, + const char* /*other_random_headers*/) { + if (from) + while (IS_SPACE(*from)) from++; + if (reply_to) + while (IS_SPACE(*reply_to)) reply_to++; + if (fcc) + while (IS_SPACE(*fcc)) fcc++; + if (followup_to) + while (IS_SPACE(*followup_to)) followup_to++; + + // TODO: sanity check other_random_headers for newline conventions + if (!from || !*from) return NS_MSG_NO_SENDER; + + return mime_sanity_check_fields_recipients(to, cc, bcc, newsgroups); +} + +// Helper macro for generating the X-Mozilla-Draft-Info header. +#define APPEND_BOOL(method, param) \ + do { \ + bool val = false; \ + fields->Get##method(&val); \ + if (val) \ + draftInfo.AppendLiteral(param "=1"); \ + else \ + draftInfo.AppendLiteral(param "=0"); \ + } while (false) + +nsresult mime_generate_headers(nsIMsgCompFields* fields, + nsMsgDeliverMode deliver_mode, + msgIWritableStructuredHeaders* finalHeaders) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isDraft = deliver_mode == nsIMsgSend::nsMsgSaveAsDraft || + deliver_mode == nsIMsgSend::nsMsgSaveAsTemplate || + deliver_mode == nsIMsgSend::nsMsgQueueForLater || + deliver_mode == nsIMsgSend::nsMsgDeliverBackground; + + bool hasDisclosedRecipient = false; + + MOZ_ASSERT(fields, "null fields"); + NS_ENSURE_ARG_POINTER(fields); + + nsTArray<RefPtr<msgIAddressObject>> from; + fields->GetAddressingHeader("From", true, from); + + // Copy all headers from the original compose field. + rv = finalHeaders->AddAllHeaders(fields); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMessageId = false; + if (NS_SUCCEEDED(fields->HasHeader("Message-ID", &hasMessageId)) && + hasMessageId) { + /* MDN request header requires to have MessageID header presented + * in the message in order to + * coorelate the MDN reports to the original message. Here will be + * the right place + */ + + bool returnReceipt = false; + fields->GetReturnReceipt(&returnReceipt); + if (returnReceipt && (deliver_mode != nsIMsgSend::nsMsgSaveAsDraft && + deliver_mode != nsIMsgSend::nsMsgSaveAsTemplate)) { + int32_t receipt_header_type = nsIMsgMdnGenerator::eDntType; + fields->GetReceiptHeaderType(&receipt_header_type); + + // nsIMsgMdnGenerator::eDntType = MDN Disposition-Notification-To: ; + // nsIMsgMdnGenerator::eRrtType = Return-Receipt-To: ; + // nsIMsgMdnGenerator::eDntRrtType = both MDN DNT and RRT headers . + if (receipt_header_type != nsIMsgMdnGenerator::eRrtType) + finalHeaders->SetAddressingHeader("Disposition-Notification-To", from); + if (receipt_header_type != nsIMsgMdnGenerator::eDntType) + finalHeaders->SetAddressingHeader("Return-Receipt-To", from); + } + } + + PRExplodedTime now; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now); + int gmtoffset = + (now.tm_params.tp_gmt_offset + now.tm_params.tp_dst_offset) / 60; + + // Use PR_FormatTimeUSEnglish() to format the date in US English format, + // then figure out what our local GMT offset is, and append it (since + // PR_FormatTimeUSEnglish() can't do that.) Generate four digit years as + // per RFC 1123 (superseding RFC 822.) + char dateString[130]; + PR_FormatTimeUSEnglish(dateString, sizeof(dateString), + "%a, %d %b %Y %H:%M:%S ", &now); + + char* entryPoint = dateString + strlen(dateString); + PR_snprintf(entryPoint, sizeof(dateString) - (entryPoint - dateString), + "%c%02d%02d" CRLF, (gmtoffset >= 0 ? '+' : '-'), + ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) / 60), + ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) % 60)); + finalHeaders->SetRawHeader("Date", nsDependentCString(dateString)); + + // X-Mozilla-Draft-Info + if (isDraft) { + nsAutoCString draftInfo; + draftInfo.AppendLiteral("internal/draft; "); + APPEND_BOOL(AttachVCard, "vcard"); + draftInfo.AppendLiteral("; "); + bool hasReturnReceipt = false; + fields->GetReturnReceipt(&hasReturnReceipt); + if (hasReturnReceipt) { + // slight change compared to 4.x; we used to use receipt= to tell + // whether the draft/template has request for either MDN or DNS or both + // return receipt; since the DNS is out of the picture we now use the + // header type + 1 to tell whether user has requested the return receipt + int32_t headerType = 0; + fields->GetReceiptHeaderType(&headerType); + draftInfo.AppendLiteral("receipt="); + draftInfo.AppendInt(headerType + 1); + } else + draftInfo.AppendLiteral("receipt=0"); + draftInfo.AppendLiteral("; "); + APPEND_BOOL(DSN, "DSN"); + draftInfo.AppendLiteral("; "); + draftInfo.AppendLiteral("uuencode=0"); + draftInfo.AppendLiteral("; "); + APPEND_BOOL(AttachmentReminder, "attachmentreminder"); + draftInfo.AppendLiteral("; "); + int32_t deliveryFormat; + fields->GetDeliveryFormat(&deliveryFormat); + draftInfo.AppendLiteral("deliveryformat="); + draftInfo.AppendInt(deliveryFormat); + + finalHeaders->SetRawHeader(HEADER_X_MOZILLA_DRAFT_INFO, draftInfo); + } + + bool sendUserAgent = false; + if (prefs) { + prefs->GetBoolPref("mailnews.headers.sendUserAgent", &sendUserAgent); + } + if (sendUserAgent) { + bool useMinimalUserAgent = false; + if (prefs) { + prefs->GetBoolPref("mailnews.headers.useMinimalUserAgent", + &useMinimalUserAgent); + } + if (useMinimalUserAgent) { + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + if (bundleService) { + nsCOMPtr<nsIStringBundle> brandBundle; + rv = bundleService->CreateBundle( + "chrome://branding/locale/brand.properties", + getter_AddRefs(brandBundle)); + if (NS_SUCCEEDED(rv)) { + nsString brandName; + brandBundle->GetStringFromName("brandFullName", brandName); + if (!brandName.IsEmpty()) + finalHeaders->SetUnstructuredHeader("User-Agent", brandName); + } + } + } else { + nsCOMPtr<nsIHttpProtocolHandler> pHTTPHandler = + do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http", &rv); + if (NS_SUCCEEDED(rv) && pHTTPHandler) { + nsAutoCString userAgentString; + // Ignore error since we're testing the return value. + mozilla::Unused << pHTTPHandler->GetUserAgent(userAgentString); + + if (!userAgentString.IsEmpty()) + finalHeaders->SetUnstructuredHeader( + "User-Agent", NS_ConvertUTF8toUTF16(userAgentString)); + } + } + } + + finalHeaders->SetUnstructuredHeader("MIME-Version", u"1.0"_ns); + + nsAutoCString newsgroups; + finalHeaders->GetRawHeader("Newsgroups", newsgroups); + if (!newsgroups.IsEmpty()) { + // Since the newsgroup header can contain data in the form of: + // "news://news.mozilla.org/netscape.test,news://news.mozilla.org/netscape.junk" + // we need to turn that into: "netscape.test,netscape.junk" + // (XXX: can it really?) + nsCOMPtr<nsINntpService> nntpService = + do_GetService("@mozilla.org/messenger/nntpservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString newsgroupsHeaderVal; + nsCString newshostHeaderVal; + rv = nntpService->GenerateNewsHeaderValsForPosting( + newsgroups, getter_Copies(newsgroupsHeaderVal), + getter_Copies(newshostHeaderVal)); + NS_ENSURE_SUCCESS(rv, rv); + finalHeaders->SetRawHeader("Newsgroups", newsgroupsHeaderVal); + + // If we are here, we are NOT going to send this now. (i.e. it is a Draft, + // Send Later file, etc...). Because of that, we need to store what the user + // typed in on the original composition window for use later when rebuilding + // the headers + if (deliver_mode != nsIMsgSend::nsMsgDeliverNow && + deliver_mode != nsIMsgSend::nsMsgSendUnsent) { + // This is going to be saved for later, that means we should just store + // what the user typed into the "Newsgroup" line in the + // HEADER_X_MOZILLA_NEWSHOST header for later use by "Send Unsent + // Messages", "Drafts" or "Templates" + finalHeaders->SetRawHeader(HEADER_X_MOZILLA_NEWSHOST, newshostHeaderVal); + } + + // Newsgroups are a recipient... + hasDisclosedRecipient = true; + } + + nsTArray<RefPtr<msgIAddressObject>> recipients; + finalHeaders->GetAddressingHeader("To", false, recipients); + hasDisclosedRecipient |= !recipients.IsEmpty(); + finalHeaders->GetAddressingHeader("Cc", false, recipients); + hasDisclosedRecipient |= !recipients.IsEmpty(); + + // If we don't have disclosed recipient (only Bcc), address the message to + // undisclosed-recipients to prevent problem with some servers + + // If we are saving the message as a draft, don't bother inserting the + // undisclosed recipients field. We'll take care of that when we really send + // the message. + if (!hasDisclosedRecipient && + (!isDraft || deliver_mode == nsIMsgSend::nsMsgQueueForLater)) { + bool bAddUndisclosedRecipients = true; + prefs->GetBoolPref("mail.compose.add_undisclosed_recipients", + &bAddUndisclosedRecipients); + if (bAddUndisclosedRecipients) { + bool hasBcc = false; + fields->HasHeader("Bcc", &hasBcc); + if (hasBcc) { + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (stringService) { + nsCOMPtr<nsIStringBundle> composeStringBundle; + rv = stringService->CreateBundle( + "chrome://messenger/locale/messengercompose/" + "composeMsgs.properties", + getter_AddRefs(composeStringBundle)); + if (NS_SUCCEEDED(rv)) { + nsString undisclosedRecipients; + rv = composeStringBundle->GetStringFromName("undisclosedRecipients", + undisclosedRecipients); + if (NS_SUCCEEDED(rv) && !undisclosedRecipients.IsEmpty()) { + nsCOMPtr<nsIMsgHeaderParser> headerParser( + mozilla::components::HeaderParser::Service()); + nsCOMPtr<msgIAddressObject> group; + nsTArray<RefPtr<msgIAddressObject>> noRecipients; + headerParser->MakeGroupObject(undisclosedRecipients, noRecipients, + getter_AddRefs(group)); + recipients.AppendElement(group); + finalHeaders->SetAddressingHeader("To", recipients); + } + } + } + } + } + } + + // We don't want to emit a Bcc header to the output. If we are saving this to + // Drafts/Sent, this is re-added later in nsMsgSend.cpp. + finalHeaders->DeleteHeader("bcc"); + + // Skip no or empty priority. + nsAutoCString priority; + rv = fields->GetRawHeader("X-Priority", priority); + if (NS_SUCCEEDED(rv) && !priority.IsEmpty()) { + nsMsgPriorityValue priorityValue; + + NS_MsgGetPriorityFromString(priority.get(), priorityValue); + + // Skip default priority. + if (priorityValue != nsMsgPriority::Default) { + nsAutoCString priorityName; + nsAutoCString priorityValueString; + + NS_MsgGetPriorityValueString(priorityValue, priorityValueString); + NS_MsgGetUntranslatedPriorityName(priorityValue, priorityName); + + // Output format: [X-Priority: <pValue> (<pName>)] + priorityValueString.AppendLiteral(" ("); + priorityValueString += priorityName; + priorityValueString.Append(')'); + finalHeaders->SetRawHeader("X-Priority", priorityValueString); + } + } + + nsAutoCString references; + finalHeaders->GetRawHeader("References", references); + if (!references.IsEmpty()) { + // The References header should be kept under 998 characters: if it's too + // long, trim out the earliest references to make it smaller. + if (references.Length() > 986) { + int32_t firstRef = references.FindChar('<'); + int32_t secondRef = references.FindChar('<', firstRef + 1); + if (secondRef > 0) { + nsAutoCString newReferences(StringHead(references, secondRef)); + int32_t bracket = references.FindChar( + '<', references.Length() + newReferences.Length() - 986); + if (bracket > 0) { + newReferences.Append(Substring(references, bracket)); + finalHeaders->SetRawHeader("References", newReferences); + } + } + } + // The In-Reply-To header is the last entry in the references header... + int32_t bracket = references.RFind("<"); + if (bracket >= 0) + finalHeaders->SetRawHeader("In-Reply-To", Substring(references, bracket)); + } + + return NS_OK; +} + +#undef APPEND_BOOL // X-Mozilla-Draft-Info helper macro + +static void GenerateGlobalRandomBytes(unsigned char* buf, int32_t len) { + // Attempt to generate bytes from system entropy-based RNG. + nsCOMPtr<nsIRandomGenerator> randomGenerator( + do_GetService("@mozilla.org/security/random-generator;1")); + MOZ_ASSERT(randomGenerator, "nsIRandomGenerator service not retrievable"); + uint8_t* tempBuffer; + nsresult rv = randomGenerator->GenerateRandomBytes(len, &tempBuffer); + if (NS_SUCCEEDED(rv)) { + memcpy(buf, tempBuffer, len); + free(tempBuffer); + return; + } + // nsIRandomGenerator failed -- fall back to low entropy PRNG. + static bool firstTime = true; + if (firstTime) { + // Seed the random-number generator with current time so that + // the numbers will be different every time we run. + srand((unsigned)PR_Now()); + firstTime = false; + } + + for (int32_t i = 0; i < len; i++) buf[i] = rand() % 256; +} + +char* mime_make_separator(const char* prefix) { + unsigned char rand_buf[13]; + GenerateGlobalRandomBytes(rand_buf, 12); + + return PR_smprintf( + "------------%s" + "%02X%02X%02X%02X" + "%02X%02X%02X%02X" + "%02X%02X%02X%02X", + prefix, rand_buf[0], rand_buf[1], rand_buf[2], rand_buf[3], rand_buf[4], + rand_buf[5], rand_buf[6], rand_buf[7], rand_buf[8], rand_buf[9], + rand_buf[10], rand_buf[11]); +} + +// Tests if the content of a string is a valid host name. +// In this case, a valid host name is any non-empty string that only contains +// letters (a-z + A-Z), numbers (0-9) and the characters '-', '_' and '.'. +static bool isValidHost(const nsCString& host) { + if (host.IsEmpty()) { + return false; + } + + const auto* cur = host.BeginReading(); + const auto* end = host.EndReading(); + for (; cur < end; ++cur) { + if (!isalpha(*cur) && !isdigit(*cur) && *cur != '-' && *cur != '_' && + *cur != '.') { + return false; + } + } + + return true; +} + +// Extract the domain name from an address. +// If none could be found (i.e. the address does not contain an '@' sign, or +// the value following it is not a valid domain), then nullptr is returned. +void msg_domain_name_from_address(const nsACString& address, nsACString& host) { + auto atIndex = address.FindChar('@'); + + if (address.IsEmpty() || atIndex == kNotFound) { + return; + } + + // Substring() should handle cases where we would go out of bounds (by + // preventing the index from exceeding the length of the source string), so we + // don't need to handle this here. + host = Substring(address, atIndex + 1); +} + +// Generate a value for a Message-Id header using the identity and optional +// hostname provided. +void msg_generate_message_id(nsIMsgIdentity* identity, + const nsACString& customHost, + nsACString& messageID) { + nsCString host; + + // Check if the identity forces host name. This is sometimes the case when + // using newsgroup. + nsCString forcedFQDN; + nsresult rv = identity->GetCharAttribute("FQDN", forcedFQDN); + if (NS_SUCCEEDED(rv) && !forcedFQDN.IsEmpty()) { + host = forcedFQDN; + } + + // If no valid host name has been set, try using the value defined by the + // caller, if any. + if (!isValidHost(host)) { + host = customHost; + } + + // If no valid host name has been set, try extracting one from the email + // address associated with the identity. + if (!isValidHost(host)) { + nsCString from; + rv = identity->GetEmail(from); + if (NS_SUCCEEDED(rv) && !from.IsEmpty()) { + msg_domain_name_from_address(from, host); + } + } + + // If we still couldn't find a valid host name to use, we can't generate a + // valid message ID, so bail, and let NNTP and SMTP generate them. + if (!isValidHost(host)) { + return; + } + + // Generate 128-bit UUID for the local part of the ID. `nsID` provides us with + // cryptographically-secure generation. + nsID uuid = nsID::GenerateUUID(); + char uuidString[NSID_LENGTH]; + uuid.ToProvidedString(uuidString); + // Drop first and last characters (curly braces). + uuidString[NSID_LENGTH - 2] = 0; + + messageID.AppendPrintf("<%s@%s>", uuidString + 1, host.get()); +} + +// this is to guarantee the folded line will never be greater +// than 78 = 75 + CRLFLWSP +#define PR_MAX_FOLDING_LEN 75 + +/*static */ char* RFC2231ParmFolding(const char* parmName, + const char* parmValue) { + NS_ENSURE_TRUE(parmName && *parmName && parmValue && *parmValue, nullptr); + + bool needEscape; + nsCString dupParm; + nsCString charset("UTF-8"); + + if (!mozilla::IsAsciiNullTerminated(parmValue)) { + needEscape = true; + dupParm.Assign(parmValue); + MsgEscapeString(dupParm, nsINetUtil::ESCAPE_ALL, dupParm); + } else { + needEscape = false; + dupParm.Adopt(msg_make_filename_qtext(parmValue, true)); + } + + int32_t parmNameLen = PL_strlen(parmName); + int32_t parmValueLen = dupParm.Length(); + + parmNameLen += 5; // *=__'__'___ or *[0]*=__'__'__ or *[1]*=___ or *[0]="___" + + char* foldedParm = nullptr; + + if ((parmValueLen + parmNameLen + strlen("UTF-8")) < PR_MAX_FOLDING_LEN) { + foldedParm = PL_strdup(parmName); + if (needEscape) { + NS_MsgSACat(&foldedParm, "*="); + NS_MsgSACat(&foldedParm, "UTF-8"); + NS_MsgSACat(&foldedParm, "''"); // We don't support language. + } else + NS_MsgSACat(&foldedParm, "=\""); + NS_MsgSACat(&foldedParm, dupParm.get()); + if (!needEscape) NS_MsgSACat(&foldedParm, "\""); + } else { + int curLineLen = 0; + int counter = 0; + char digits[32]; + char* start = dupParm.BeginWriting(); + char* end = NULL; + char tmp = 0; + + while (parmValueLen > 0) { + curLineLen = 0; + if (counter == 0) { + PR_FREEIF(foldedParm) + foldedParm = PL_strdup(parmName); + } else { + NS_MsgSACat(&foldedParm, ";\r\n "); + NS_MsgSACat(&foldedParm, parmName); + } + PR_snprintf(digits, sizeof(digits), "*%d", counter); + NS_MsgSACat(&foldedParm, digits); + curLineLen += PL_strlen(digits); + if (needEscape) { + NS_MsgSACat(&foldedParm, "*="); + if (counter == 0) { + NS_MsgSACat(&foldedParm, "UTF-8"); + NS_MsgSACat(&foldedParm, "''"); // We don't support language. + curLineLen += strlen("UTF-8"); + } + } else { + NS_MsgSACat(&foldedParm, "=\""); + } + counter++; + curLineLen += parmNameLen; + if (parmValueLen <= PR_MAX_FOLDING_LEN - curLineLen) + end = start + parmValueLen; + else + end = start + (PR_MAX_FOLDING_LEN - curLineLen); + + tmp = 0; + if (*end && needEscape) { + // Check to see if we are in the middle of escaped char. + // We use ESCAPE_ALL, so every third character is a '%'. + if (end - 1 > start && *(end - 1) == '%') { + end -= 1; + } else if (end - 2 > start && *(end - 2) == '%') { + end -= 2; + } + // *end is now a '%'. + // Check if the following UTF-8 octet is a continuation. + while (end - 3 > start && (*(end + 1) == '8' || *(end + 1) == '9' || + *(end + 1) == 'A' || *(end + 1) == 'B')) { + end -= 3; + } + tmp = *end; + *end = 0; + } else { + tmp = *end; + *end = 0; + } + NS_MsgSACat(&foldedParm, start); + if (!needEscape) NS_MsgSACat(&foldedParm, "\""); + + parmValueLen -= (end - start); + if (tmp) *end = tmp; + start = end; + } + } + + return foldedParm; +} + +bool mime_7bit_data_p(const char* string, uint32_t size) { + if ((!string) || (!*string)) return true; + + char* ptr = (char*)string; + for (uint32_t i = 0; i < size; i++) { + if ((unsigned char)ptr[i] > 0x7F) return false; + } + return true; +} + +// Strips whitespace, and expands newlines into newline-tab for use in +// mail headers. Returns a new string or 0 (if it would have been empty.) +// If addr_p is true, the addresses will be parsed and reemitted as +// rfc822 mailboxes. +char* mime_fix_header_1(const char* string, bool addr_p, bool news_p) { + char* new_string; + const char* in; + char* out; + int32_t i, old_size, new_size; + + if (!string || !*string) return 0; + + if (addr_p) { + return strdup(string); + } + + old_size = PL_strlen(string); + new_size = old_size; + for (i = 0; i < old_size; i++) + if (string[i] == '\r' || string[i] == '\n') new_size += 2; + + new_string = (char*)PR_Malloc(new_size + 1); + if (!new_string) return 0; + + in = string; + out = new_string; + + /* strip leading whitespace. */ + while (IS_SPACE(*in)) in++; + + /* replace CR, LF, or CRLF with CRLF-TAB. */ + while (*in) { + if (*in == '\r' || *in == '\n') { + if (*in == '\r' && in[1] == '\n') in++; + in++; + *out++ = '\r'; + *out++ = '\n'; + *out++ = '\t'; + } else if (news_p && *in == ',') { + *out++ = *in++; + /* skip over all whitespace after a comma. */ + while (IS_SPACE(*in)) in++; + } else + *out++ = *in++; + } + *out = 0; + + /* strip trailing whitespace. */ + while (out > in && IS_SPACE(out[-1])) *out-- = 0; + + /* If we ended up throwing it all away, use 0 instead of "". */ + if (!*new_string) { + PR_Free(new_string); + new_string = 0; + } + + return new_string; +} + +char* mime_fix_header(const char* string) { + return mime_fix_header_1(string, false, false); +} + +char* mime_fix_addr_header(const char* string) { + return mime_fix_header_1(string, true, false); +} + +char* mime_fix_news_header(const char* string) { + return mime_fix_header_1(string, false, true); +} + +bool mime_type_requires_b64_p(const char* type) { + if (!type || !PL_strcasecmp(type, UNKNOWN_CONTENT_TYPE)) + // Unknown types don't necessarily require encoding. (Note that + // "unknown" and "application/octet-stream" aren't the same.) + return false; + + else if (!PL_strncasecmp(type, "image/", 6) || + !PL_strncasecmp(type, "audio/", 6) || + !PL_strncasecmp(type, "video/", 6) || + !PL_strncasecmp(type, "application/", 12)) { + // The following types are application/ or image/ types that are actually + // known to contain textual data (meaning line-based, not binary, where + // CRLF conversion is desired rather than disastrous.) So, if the type + // is any of these, it does not *require* base64, and if we do need to + // encode it for other reasons, we'll probably use quoted-printable. + // But, if it's not one of these types, then we assume that any subtypes + // of the non-"text/" types are binary data, where CRLF conversion would + // corrupt it, so we use base64 right off the bat. + + // The reason it's desirable to ship these as text instead of just using + // base64 all the time is mainly to preserve the readability of them for + // non-MIME users: if I mail a /bin/sh script to someone, it might not + // need to be encoded at all, so we should leave it readable if we can. + + // This list of types was derived from the comp.mail.mime FAQ, section + // 10.2.2, "List of known unregistered MIME types" on 2-Feb-96. + static const char* app_and_image_types_which_are_really_text[] = { + "application/mac-binhex40", /* APPLICATION_BINHEX */ + "application/pgp", /* APPLICATION_PGP */ + "application/pgp-keys", + "application/x-pgp-message", /* APPLICATION_PGP2 */ + "application/postscript", /* APPLICATION_POSTSCRIPT */ + "application/x-uuencode", /* APPLICATION_UUENCODE */ + "application/x-uue", /* APPLICATION_UUENCODE2 */ + "application/uue", /* APPLICATION_UUENCODE4 */ + "application/uuencode", /* APPLICATION_UUENCODE3 */ + "application/sgml", + "application/x-csh", + "application/javascript", + "application/ecmascript", + "application/x-javascript", + "application/x-latex", + "application/x-macbinhex40", + "application/x-ns-proxy-autoconfig", + "application/x-www-form-urlencoded", + "application/x-perl", + "application/x-sh", + "application/x-shar", + "application/x-tcl", + "application/x-tex", + "application/x-texinfo", + "application/x-troff", + "application/x-troff-man", + "application/x-troff-me", + "application/x-troff-ms", + "application/x-troff-ms", + "application/x-wais-source", + "image/x-bitmap", + "image/x-pbm", + "image/x-pgm", + "image/x-portable-anymap", + "image/x-portable-bitmap", + "image/x-portable-graymap", + "image/x-portable-pixmap", /* IMAGE_PPM */ + "image/x-ppm", + "image/x-xbitmap", /* IMAGE_XBM */ + "image/x-xbm", /* IMAGE_XBM2 */ + "image/xbm", /* IMAGE_XBM3 */ + "image/x-xpixmap", + "image/x-xpm", + 0}; + const char** s; + for (s = app_and_image_types_which_are_really_text; *s; s++) + if (!PL_strcasecmp(type, *s)) return false; + + /* All others must be assumed to be binary formats, and need Base64. */ + return true; + } + + else + return false; +} + +// +// Some types should have a "charset=" parameter, and some shouldn't. +// This is what decides. +// +bool mime_type_needs_charset(const char* type) { + /* Only text types should have charset. */ + if (!type || !*type) + return false; + else if (!PL_strncasecmp(type, "text", 4)) + return true; + else + return false; +} + +// Given a string, convert it to 'qtext' (quoted text) for RFC822 header +// purposes. +char* msg_make_filename_qtext(const char* srcText, bool stripCRLFs) { + /* newString can be at most twice the original string (every char quoted). */ + char* newString = (char*)PR_Malloc(PL_strlen(srcText) * 2 + 1); + if (!newString) return NULL; + + const char* s = srcText; + const char* end = srcText + PL_strlen(srcText); + char* d = newString; + + while (*s) { + // Put backslashes in front of existing backslashes, or double quote + // characters. + // If stripCRLFs is true, don't write out CRs or LFs. Otherwise, + // write out a backslash followed by the CR but not + // linear-white-space. + // We might already have quoted pair of "\ " or "\\t" skip it. + if (*s == '\\' || *s == '"' || + (!stripCRLFs && + (*s == '\r' && (s[1] != '\n' || + (s[1] == '\n' && (s + 2) < end && !IS_SPACE(s[2])))))) + *d++ = '\\'; + + if (stripCRLFs && *s == '\r' && s[1] == '\n' && (s + 2) < end && + IS_SPACE(s[2])) { + s += 3; // skip CRLFLWSP + } else { + *d++ = *s++; + } + } + *d = 0; + + return newString; +} + +// Utility to create a nsIURI object... +nsresult nsMsgNewURL(nsIURI** aInstancePtrResult, const nsCString& aSpec) { + nsresult rv = NS_OK; + if (nullptr == aInstancePtrResult) return NS_ERROR_NULL_POINTER; + nsCOMPtr<nsIIOService> pNetService = mozilla::components::IO::Service(); + NS_ENSURE_TRUE(pNetService, NS_ERROR_UNEXPECTED); + if (aSpec.Find("://") == kNotFound && !StringBeginsWith(aSpec, "data:"_ns)) { + // XXXjag Temporary fix for bug 139362 until the real problem(bug 70083) get + // fixed + nsAutoCString uri("http://"_ns); + uri.Append(aSpec); + rv = pNetService->NewURI(uri, nullptr, nullptr, aInstancePtrResult); + } else + rv = pNetService->NewURI(aSpec, nullptr, nullptr, aInstancePtrResult); + return rv; +} + +char* nsMsgGetLocalFileFromURL(const char* url) { + char* finalPath; + NS_ASSERTION(PL_strncasecmp(url, "file://", 7) == 0, "invalid url"); + finalPath = (char*)PR_Malloc(strlen(url)); + if (finalPath == NULL) return NULL; + strcpy(finalPath, url + 6 + 1); + return finalPath; +} + +char* nsMsgParseURLHost(const char* url) { + nsIURI* workURI = nullptr; + nsresult rv; + + rv = nsMsgNewURL(&workURI, nsDependentCString(url)); + if (NS_FAILED(rv) || !workURI) return nullptr; + + nsAutoCString host; + rv = workURI->GetHost(host); + NS_IF_RELEASE(workURI); + if (NS_FAILED(rv)) return nullptr; + + return ToNewCString(host); +} + +char* GenerateFileNameFromURI(nsIURI* aURL) { + nsresult rv; + nsCString file; + nsCString spec; + char* returnString; + char* cp = nullptr; + char* cp1 = nullptr; + + rv = aURL->GetPathQueryRef(file); + if (NS_SUCCEEDED(rv) && !file.IsEmpty()) { + char* newFile = ToNewCString(file); + if (!newFile) return nullptr; + + // strip '/' + cp = PL_strrchr(newFile, '/'); + if (cp) + ++cp; + else + cp = newFile; + + if (*cp) { + if ((cp1 = PL_strchr(cp, '/'))) *cp1 = 0; + if ((cp1 = PL_strchr(cp, '?'))) *cp1 = 0; + if ((cp1 = PL_strchr(cp, '>'))) *cp1 = 0; + if (*cp != '\0') { + returnString = PL_strdup(cp); + PR_FREEIF(newFile); + return returnString; + } + } else + return nullptr; + } + + cp = nullptr; + cp1 = nullptr; + + rv = aURL->GetSpec(spec); + if (NS_SUCCEEDED(rv) && !spec.IsEmpty()) { + char* newSpec = ToNewCString(spec); + if (!newSpec) return nullptr; + + char *cp2 = NULL, *cp3 = NULL; + + // strip '"' + cp2 = newSpec; + while (*cp2 == '"') cp2++; + if ((cp3 = PL_strchr(cp2, '"'))) *cp3 = 0; + + char* hostStr = nsMsgParseURLHost(cp2); + if (!hostStr) hostStr = PL_strdup(cp2); + + bool isHTTP = false; + if (NS_SUCCEEDED(aURL->SchemeIs("http", &isHTTP)) && isHTTP) { + returnString = PR_smprintf("%s.html", hostStr); + PR_FREEIF(hostStr); + } else + returnString = hostStr; + + PR_FREEIF(newSpec); + return returnString; + } + + return nullptr; +} + +// +// This routine will generate a content id for use in a mail part. +// It will take the part number passed in as well as the email +// address. If the email address is null or invalid, we will simply +// use netscape.com for the interesting part. The content ID's will +// look like the following: +// +// Content-ID: <part1.36DF1DCE.73B5A330@netscape.com> +// +char* mime_gen_content_id(uint32_t aPartNum, const char* aEmailAddress) { + int32_t randLen = 5; + unsigned char rand_buf1[5]; + unsigned char rand_buf2[5]; + const char* domain = nullptr; + const char* defaultDomain = "@netscape.com"; + + memset(rand_buf1, 0, randLen - 1); + memset(rand_buf2, 0, randLen - 1); + + GenerateGlobalRandomBytes(rand_buf1, randLen); + GenerateGlobalRandomBytes(rand_buf2, randLen); + + // Find the @domain.com string... + if (aEmailAddress && *aEmailAddress) + domain = const_cast<const char*>(PL_strchr(aEmailAddress, '@')); + + if (!domain) domain = defaultDomain; + + char* retVal = PR_smprintf( + "part%d." + "%02X%02X%02X%02X" + "." + "%02X%02X%02X%02X" + "%s", + aPartNum, rand_buf1[0], rand_buf1[1], rand_buf1[2], rand_buf1[3], + rand_buf2[0], rand_buf2[1], rand_buf2[2], rand_buf2[3], domain); + + return retVal; +} + +void GetFolderURIFromUserPrefs(nsMsgDeliverMode aMode, nsIMsgIdentity* identity, + nsCString& uri) { + nsresult rv; + uri.Truncate(); + + // QueueForLater (Outbox) + if (aMode == nsIMsgSend::nsMsgQueueForLater || + aMode == nsIMsgSend::nsMsgDeliverBackground) { + nsCOMPtr<nsIPrefBranch> prefs( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return; + rv = prefs->GetCharPref("mail.default_sendlater_uri", uri); + if (NS_FAILED(rv) || uri.IsEmpty()) + uri.AssignLiteral(ANY_SERVER); + else { + // check if uri is unescaped, and if so, escape it and reset the pef. + if (uri.FindChar(' ') != kNotFound) { + uri.ReplaceSubstring(" ", "%20"); + prefs->SetCharPref("mail.default_sendlater_uri", uri); + } + } + return; + } + + if (!identity) return; + + if (aMode == nsIMsgSend::nsMsgSaveAsDraft) // SaveAsDraft (Drafts) + rv = identity->GetDraftFolder(uri); + else if (aMode == + nsIMsgSend::nsMsgSaveAsTemplate) // SaveAsTemplate (Templates) + rv = identity->GetStationeryFolder(uri); + else { + bool doFcc = false; + rv = identity->GetDoFcc(&doFcc); + if (doFcc) rv = identity->GetFccFolder(uri); + } + return; +} + +/** + * Check if we should use format=flowed (RFC 2646) for a mail. + * We will use format=flowed unless the preference tells us not to do so. + * In this function we set all the serialiser flags. + * 'formatted' is always 'true'. + */ +void GetSerialiserFlags(bool* flowed, bool* formatted) { + *flowed = false; + *formatted = true; + + // Set format=flowed as in RFC 2646 according to the preference. + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + prefs->GetBoolPref("mailnews.send_plaintext_flowed", flowed); + } +} + +already_AddRefed<nsIArray> GetEmbeddedObjects( + mozilla::dom::Document* aDocument) { + nsCOMPtr<nsIMutableArray> nodes = do_CreateInstance(NS_ARRAY_CONTRACTID); + if (NS_WARN_IF(!nodes)) { + return nullptr; + } + + mozilla::PostContentIterator iter; + nsresult rv = iter.Init(aDocument->GetRootElement()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // Loop through the content iterator for each content node. + while (!iter.IsDone()) { + nsINode* node = iter.GetCurrentNode(); + if (node->IsElement()) { + mozilla::dom::Element* element = node->AsElement(); + + // See if it's an image or also include all links. + // Let mail decide which link to send or not + if (element->IsAnyOfHTMLElements(nsGkAtoms::img, nsGkAtoms::a) || + (element->IsHTMLElement(nsGkAtoms::body) && + element->HasAttr(kNameSpaceID_None, nsGkAtoms::background))) { + nodes->AppendElement(node); + } + } + iter.Next(); + } + + return nodes.forget(); +} diff --git a/comm/mailnews/compose/src/nsMsgCompUtils.h b/comm/mailnews/compose/src/nsMsgCompUtils.h new file mode 100644 index 0000000000..7197cde459 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCompUtils.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgCompUtils_H_ +#define _nsMsgCompUtils_H_ + +#include "nscore.h" +#include "mozilla/dom/Document.h" +#include "nsMsgCompFields.h" +#include "nsIMsgSend.h" +#include "nsIMsgCompUtils.h" + +class nsIArray; +class nsIDocument; +class nsIPrompt; + +#define ANY_SERVER "anyfolder://" + +// these are msg hdr property names for storing the original +// msg uri's and disposition(replied/forwarded) when queuing +// messages to send later. +#define ORIG_URI_PROPERTY "origURIs" +#define QUEUED_DISPOSITION_PROPERTY "queuedDisposition" + +extern mozilla::LazyLogModule Compose; + +class nsMsgCompUtils : public nsIMsgCompUtils { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGCOMPUTILS + + nsMsgCompUtils(); + + private: + virtual ~nsMsgCompUtils(); +}; + +already_AddRefed<nsIArray> GetEmbeddedObjects( + mozilla::dom::Document* aDocument); + +PR_BEGIN_EXTERN_C + +// +// Create a file spec or file name using the name passed +// in as a template +// +nsresult nsMsgCreateTempFile(const char* tFileName, nsIFile** tFile); +char* nsMsgCreateTempFileName(const char* tFileName); + +// +// Various utilities for building parts of MIME encoded +// messages during message composition +// + +nsresult mime_sanity_check_fields_recipients(const char* to, const char* cc, + const char* bcc, + const char* newsgroups); + +nsresult mime_sanity_check_fields( + const char* from, const char* reply_to, const char* to, const char* cc, + const char* bcc, const char* fcc, const char* newsgroups, + const char* followup_to, const char* /*subject*/, + const char* /*references*/, const char* /*organization*/, + const char* /*other_random_headers*/); + +nsresult mime_generate_headers(nsIMsgCompFields* fields, + nsMsgDeliverMode deliver_mode, + msgIWritableStructuredHeaders* headers); + +char* mime_make_separator(const char* prefix); +char* mime_gen_content_id(uint32_t aPartNum, const char* aEmailAddress); + +bool mime_7bit_data_p(const char* string, uint32_t size); + +char* mime_fix_header_1(const char* string, bool addr_p, bool news_p); +char* mime_fix_header(const char* string); +char* mime_fix_addr_header(const char* string); +char* mime_fix_news_header(const char* string); + +bool mime_type_requires_b64_p(const char* type); +bool mime_type_needs_charset(const char* type); + +char* msg_make_filename_qtext(const char* srcText, bool stripCRLFs); + +char* RFC2231ParmFolding(const char* parmName, const char* parmValue); + +// +// Informational calls... +// +void nsMsgMIMESetConformToStandard(bool conform_p); +bool nsMsgMIMEGetConformToStandard(void); + +// +// network service type calls... +// +nsresult nsMsgNewURL(nsIURI** aInstancePtrResult, const nsCString& aSpec); +char* nsMsgGetLocalFileFromURL(const char* url); + +char* nsMsgParseURLHost(const char* url); + +char* GenerateFileNameFromURI(nsIURI* aURL); + +// +// Folder calls... +// +void GetFolderURIFromUserPrefs(nsMsgDeliverMode aMode, nsIMsgIdentity* identity, + nsCString& uri); + +// Check if we should use format=flowed +void GetSerialiserFlags(bool* flowed, bool* formatted); + +PR_END_EXTERN_C + +#endif /* _nsMsgCompUtils_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgCompose.cpp b/comm/mailnews/compose/src/nsMsgCompose.cpp new file mode 100644 index 0000000000..2630852f5a --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCompose.cpp @@ -0,0 +1,5094 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgCompose.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "mozIDOMWindow.h" +#include "nsISelectionController.h" +#include "nsMsgI18N.h" +#include "nsMsgQuote.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIDocumentEncoder.h" // for editor output flags +#include "nsMsgCompUtils.h" +#include "nsComposeStrings.h" +#include "nsIMsgSend.h" +#include "nsMailHeaders.h" +#include "nsMsgPrompts.h" +#include "nsMimeTypes.h" +#include "nsICharsetConverterManager.h" +#include "nsTextFormatter.h" +#include "nsIHTMLEditor.h" +#include "nsIEditor.h" +#include "plstr.h" +#include "prmem.h" +#include "nsIDocShell.h" +#include "nsCExternalHandlerService.h" +#include "nsIMIMEService.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIWindowMediator.h" +#include "nsIURL.h" +#include "mozilla/intl/AppDateTimeFormat.h" +#include "nsIMsgComposeService.h" +#include "nsIMsgComposeProgressParams.h" +#include "nsMsgUtils.h" +#include "nsIMsgImapMailFolder.h" +#include "nsImapCore.h" +#include "nsUnicharUtils.h" +#include "nsNetUtil.h" +#include "nsIContentViewer.h" +#include "nsIMsgMdnGenerator.h" +#include "plbase64.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgAttachment.h" +#include "nsIMsgProgress.h" +#include "nsMsgFolderFlags.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgDatabase.h" +#include "nsStringStream.h" +#include "nsArrayUtils.h" +#include "nsIMsgWindow.h" +#include "nsITextToSubURI.h" +#include "nsIAbManager.h" +#include "nsCRT.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/Components.h" +#include "mozilla/Services.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "mozilla/Preferences.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/Utf8.h" +#include "nsStreamConverter.h" +#include "nsIObserverService.h" +#include "nsIProtocolHandler.h" +#include "nsContentUtils.h" +#include "nsStreamUtils.h" +#include "nsIFileURL.h" +#include "nsTextNode.h" // from dom/base +#include "nsIParserUtils.h" +#include "nsIStringBundle.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::mailnews; + +LazyLogModule Compose("Compose"); + +static nsresult GetReplyHeaderInfo(int32_t* reply_header_type, + nsString& reply_header_authorwrote, + nsString& reply_header_ondateauthorwrote, + nsString& reply_header_authorwroteondate, + nsString& reply_header_originalmessage) { + nsresult rv; + *reply_header_type = 0; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // If fetching any of the preferences fails, + // we return early with header_type = 0 meaning "no header". + rv = NS_GetLocalizedUnicharPreference( + prefBranch, "mailnews.reply_header_authorwrotesingle", + reply_header_authorwrote); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_GetLocalizedUnicharPreference( + prefBranch, "mailnews.reply_header_ondateauthorwrote", + reply_header_ondateauthorwrote); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_GetLocalizedUnicharPreference( + prefBranch, "mailnews.reply_header_authorwroteondate", + reply_header_authorwroteondate); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_GetLocalizedUnicharPreference(prefBranch, + "mailnews.reply_header_originalmessage", + reply_header_originalmessage); + NS_ENSURE_SUCCESS(rv, rv); + + return prefBranch->GetIntPref("mailnews.reply_header_type", + reply_header_type); +} + +static void TranslateLineEnding(nsString& data) { + char16_t* rPtr; // Read pointer + char16_t* wPtr; // Write pointer + char16_t* sPtr; // Start data pointer + char16_t* ePtr; // End data pointer + + rPtr = wPtr = sPtr = data.BeginWriting(); + ePtr = rPtr + data.Length(); + + while (rPtr < ePtr) { + if (*rPtr == nsCRT::CR) { + *wPtr = nsCRT::LF; + if (rPtr + 1 < ePtr && *(rPtr + 1) == nsCRT::LF) rPtr++; + } else + *wPtr = *rPtr; + + rPtr++; + wPtr++; + } + + data.SetLength(wPtr - sPtr); +} + +nsMsgCompose::nsMsgCompose() { + mQuotingToFollow = false; + mAllowRemoteContent = false; + mWhatHolder = 1; + m_window = nullptr; + m_editor = nullptr; + mQuoteStreamListener = nullptr; + mAutodetectCharset = false; + mDeleteDraft = false; + m_compFields = + nullptr; // m_compFields will be set during nsMsgCompose::Initialize + mType = nsIMsgCompType::New; + + // For TagConvertible + // Read and cache pref + mConvertStructs = false; + nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) + prefBranch->GetBoolPref("converter.html2txt.structs", &mConvertStructs); + + m_composeHTML = false; + + mTmpAttachmentsDeleted = false; + mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_None; + mDeliverMode = 0; +} + +nsMsgCompose::~nsMsgCompose() { + MOZ_LOG(Compose, LogLevel::Debug, ("~nsMsgCompose()")); + if (!m_compFields) { + // Uhoh. We're in an uninitialized state. Maybe initialize() failed, or + // was never even called. + return; + } + m_window = nullptr; + if (!mMsgSend) { + // This dtor can be called before mMsgSend->CreateAndSendMessage returns, + // tmp attachments are needed to create the message, so don't delete them. + DeleteTmpAttachments(); + } +} + +/* the following macro actually implement addref, release and query interface + * for our component. */ +NS_IMPL_ISUPPORTS(nsMsgCompose, nsIMsgCompose, nsIMsgSendListener, + nsISupportsWeakReference) + +// +// Once we are here, convert the data which we know to be UTF-8 to UTF-16 +// for insertion into the editor +// +nsresult GetChildOffset(nsINode* aChild, nsINode* aParent, int32_t& aOffset) { + NS_ASSERTION((aChild && aParent), "bad args"); + + if (!aChild || !aParent) return NS_ERROR_NULL_POINTER; + + nsINodeList* childNodes = aParent->ChildNodes(); + for (uint32_t i = 0; i < childNodes->Length(); i++) { + nsINode* childNode = childNodes->Item(i); + if (childNode == aChild) { + aOffset = i; + return NS_OK; + } + } + + return NS_ERROR_NULL_POINTER; +} + +nsresult GetNodeLocation(nsINode* inChild, nsCOMPtr<nsINode>* outParent, + int32_t* outOffset) { + NS_ASSERTION((outParent && outOffset), "bad args"); + nsresult result = NS_ERROR_NULL_POINTER; + if (inChild && outParent && outOffset) { + nsCOMPtr<nsINode> inChild2 = inChild; + *outParent = inChild2->GetParentNode(); + if (*outParent) { + result = GetChildOffset(inChild2, *outParent, *outOffset); + } + } + + return result; +} + +bool nsMsgCompose::IsEmbeddedObjectSafe(const char* originalScheme, + const char* originalHost, + const char* originalPath, + Element* element) { + nsresult rv; + + nsAutoString objURL; + + if (!originalScheme || !originalPath) // Having a null host is OK. + return false; + + RefPtr<HTMLImageElement> image = HTMLImageElement::FromNode(element); + RefPtr<HTMLAnchorElement> anchor = HTMLAnchorElement::FromNode(element); + + if (image) + image->GetSrc(objURL); + else if (anchor) + anchor->GetHref(objURL); + else + return false; + + if (!objURL.IsEmpty()) { + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), objURL); + if (NS_SUCCEEDED(rv) && uri) { + nsAutoCString scheme; + rv = uri->GetScheme(scheme); + if (NS_SUCCEEDED(rv) && + scheme.Equals(originalScheme, nsCaseInsensitiveCStringComparator)) { + nsAutoCString host; + rv = uri->GetAsciiHost(host); + // mailbox url don't have a host therefore don't be too strict. + if (NS_SUCCEEDED(rv) && + (host.IsEmpty() || originalHost || + host.Equals(originalHost, nsCaseInsensitiveCStringComparator))) { + nsAutoCString path; + rv = uri->GetPathQueryRef(path); + if (NS_SUCCEEDED(rv)) { + nsAutoCString orgPath(originalPath); + MsgRemoveQueryPart(orgPath); + MsgRemoveQueryPart(path); + // mailbox: and JS Account URLs have a message number in + // the query part of "path query ref". We removed this so + // we're not comparing down to the message but down to the folder. + // Code in the frontend (in the "error" event listener in + // MsgComposeCommands.js that deals with unblocking images) will + // prompt if a part of another message is referenced. + // A saved message opened for reply or forwarding has a + // mailbox: URL. + // imap: URLs don't have the message number in the query, so we do + // compare it here. + // news: URLs use group and key in the query, but it's OK to compare + // without them. + return path.Equals(orgPath, nsCaseInsensitiveCStringComparator); + } + } + } + } + } + + return false; +} + +/* The purpose of this function is to mark any embedded object that wasn't a + RFC822 part of the original message as moz-do-not-send. That will prevent us + to attach data not specified by the user or not present in the original + message. +*/ +nsresult nsMsgCompose::TagEmbeddedObjects(nsIEditor* aEditor) { + nsresult rv = NS_OK; + uint32_t count; + uint32_t i; + + if (!aEditor) return NS_ERROR_FAILURE; + + nsCOMPtr<Document> document; + aEditor->GetDocument(getter_AddRefs(document)); + if (!document) return NS_ERROR_FAILURE; + nsCOMPtr<nsIArray> aNodeList = GetEmbeddedObjects(document); + if (!aNodeList) return NS_ERROR_FAILURE; + + if (NS_FAILED(aNodeList->GetLength(&count))) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIURI> originalUrl; + nsCString originalScheme; + nsCString originalHost; + nsCString originalPath; + + // first, convert the rdf original msg uri into a url that represents the + // message... + nsCOMPtr<nsIMsgMessageService> msgService; + rv = GetMessageServiceFromURI(mOriginalMsgURI, getter_AddRefs(msgService)); + if (NS_SUCCEEDED(rv)) { + rv = msgService->GetUrlForUri(mOriginalMsgURI, nullptr, + getter_AddRefs(originalUrl)); + if (NS_SUCCEEDED(rv) && originalUrl) { + originalUrl->GetScheme(originalScheme); + originalUrl->GetAsciiHost(originalHost); + originalUrl->GetPathQueryRef(originalPath); + } + } + + // Then compare the url of each embedded objects with the original message. + // If they a not coming from the original message, they should not be sent + // with the message. + for (i = 0; i < count; i++) { + nsCOMPtr<Element> domElement = do_QueryElementAt(aNodeList, i); + if (!domElement) continue; + if (IsEmbeddedObjectSafe(originalScheme.get(), originalHost.get(), + originalPath.get(), domElement)) + continue; // Don't need to tag this object, it's safe to send it. + + // The source of this object should not be sent with the message. + IgnoredErrorResult rv2; + domElement->SetAttribute(u"moz-do-not-send"_ns, u"true"_ns, rv2); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::GetAllowRemoteContent(bool* aAllowRemoteContent) { + NS_ENSURE_ARG_POINTER(aAllowRemoteContent); + *aAllowRemoteContent = mAllowRemoteContent; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::SetAllowRemoteContent(bool aAllowRemoteContent) { + mAllowRemoteContent = aAllowRemoteContent; + return NS_OK; +} + +void nsMsgCompose::InsertDivWrappedTextAtSelection(const nsAString& aText, + const nsAString& classStr) { + NS_ASSERTION(m_editor, + "InsertDivWrappedTextAtSelection called, but no editor exists"); + if (!m_editor) return; + + RefPtr<Element> divElem; + nsCOMPtr<nsIHTMLEditor> htmlEditor(do_QueryInterface(m_editor)); + + nsresult rv = + htmlEditor->CreateElementWithDefaults(u"div"_ns, getter_AddRefs(divElem)); + + NS_ENSURE_SUCCESS_VOID(rv); + + // We need the document + nsCOMPtr<Document> doc; + rv = m_editor->GetDocument(getter_AddRefs(doc)); + NS_ENSURE_SUCCESS_VOID(rv); + + // Break up the text by newlines, and then insert text nodes followed + // by <br> nodes. + int32_t start = 0; + int32_t end = aText.Length(); + + for (;;) { + int32_t delimiter = aText.FindChar('\n', start); + if (delimiter == kNotFound) delimiter = end; + + RefPtr<nsTextNode> textNode = + doc->CreateTextNode(Substring(aText, start, delimiter - start)); + + IgnoredErrorResult rv2; + divElem->AppendChild(*textNode, rv2); + if (rv2.Failed()) { + return; + } + + // Now create and insert a BR + RefPtr<Element> brElem; + rv = + htmlEditor->CreateElementWithDefaults(u"br"_ns, getter_AddRefs(brElem)); + NS_ENSURE_SUCCESS_VOID(rv); + divElem->AppendChild(*brElem, rv2); + if (rv2.Failed()) { + return; + } + + if (delimiter == end) break; + start = ++delimiter; + if (start == end) break; + } + + htmlEditor->InsertElementAtSelection(divElem, true); + nsCOMPtr<nsINode> parent; + int32_t offset; + + rv = GetNodeLocation(divElem, address_of(parent), &offset); + if (NS_SUCCEEDED(rv)) { + RefPtr<Selection> selection; + m_editor->GetSelection(getter_AddRefs(selection)); + + if (selection) selection->CollapseInLimiter(parent, offset + 1); + } + if (divElem) { + RefPtr<Element> divElem2 = divElem; + IgnoredErrorResult rv2; + divElem2->SetAttribute(u"class"_ns, classStr, rv2); + } +} + +/* + * The following function replaces <plaintext> tags with <x-plaintext>. + * <plaintext> is a funny beast: It leads to everything following it + * being displayed verbatim, even a </plaintext> tag is ignored. + */ +static void remove_plaintext_tag(nsString& body) { + // Replace all <plaintext> and </plaintext> tags. + int32_t index = 0; + bool replaced = false; + while ((index = body.LowerCaseFindASCII("<plaintext", index)) != kNotFound) { + body.Insert(u"x-", index + 1); + index += 12; + replaced = true; + } + if (replaced) { + index = 0; + while ((index = body.LowerCaseFindASCII("</plaintext", index)) != + kNotFound) { + body.Insert(u"x-", index + 2); + index += 13; + } + } +} + +static void remove_conditional_CSS(const nsAString& in, nsAString& out) { + nsCOMPtr<nsIParserUtils> parserUtils = + do_GetService(NS_PARSERUTILS_CONTRACTID); + parserUtils->RemoveConditionalCSS(in, out); +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsMsgCompose::ConvertAndLoadComposeWindow(nsString& aPrefix, nsString& aBuf, + nsString& aSignature, bool aQuoted, + bool aHTMLEditor) { + NS_ASSERTION(m_editor, "ConvertAndLoadComposeWindow but no editor"); + NS_ENSURE_TRUE(m_editor && m_identity, NS_ERROR_NOT_INITIALIZED); + + // First, get the nsIEditor interface for future use + nsCOMPtr<nsINode> nodeInserted; + + TranslateLineEnding(aPrefix); + TranslateLineEnding(aBuf); + TranslateLineEnding(aSignature); + + m_editor->EnableUndo(false); + + // Ok - now we need to figure out the charset of the aBuf we are going to send + // into the editor shell. There are I18N calls to sniff the data and then we + // need to call the new routine in the editor that will allow us to send in + // the charset + // + + // Now, insert it into the editor... + RefPtr<HTMLEditor> htmlEditor = m_editor->AsHTMLEditor(); + int32_t reply_on_top = 0; + bool sig_bottom = true; + m_identity->GetReplyOnTop(&reply_on_top); + m_identity->GetSigBottom(&sig_bottom); + bool sigOnTop = (reply_on_top == 1 && !sig_bottom); + bool isForwarded = (mType == nsIMsgCompType::ForwardInline); + + // When in paragraph mode, don't call InsertLineBreak() since that inserts + // a full paragraph instead of just a line break since we switched + // the default paragraph separator to "p". + bool paragraphMode = + mozilla::Preferences::GetBool("mail.compose.default_to_paragraph", false); + + if (aQuoted) { + if (!aPrefix.IsEmpty()) { + if (!aHTMLEditor) aPrefix.AppendLiteral("\n"); + + int32_t reply_on_top = 0; + m_identity->GetReplyOnTop(&reply_on_top); + if (reply_on_top == 1) { + // HTML editor eats one line break but not a whole paragraph. + if (aHTMLEditor && !paragraphMode) htmlEditor->InsertLineBreak(); + + // add one newline if a signature comes before the quote, two otherwise + bool includeSignature = true; + bool sig_bottom = true; + bool attachFile = false; + nsString prefSigText; + + m_identity->GetSigOnReply(&includeSignature); + m_identity->GetSigBottom(&sig_bottom); + m_identity->GetHtmlSigText(prefSigText); + nsresult rv = m_identity->GetAttachSignature(&attachFile); + if (!paragraphMode || !aHTMLEditor) { + if (includeSignature && !sig_bottom && + ((NS_SUCCEEDED(rv) && attachFile) || !prefSigText.IsEmpty())) + htmlEditor->InsertLineBreak(); + else { + htmlEditor->InsertLineBreak(); + htmlEditor->InsertLineBreak(); + } + } + } + + InsertDivWrappedTextAtSelection(aPrefix, u"moz-cite-prefix"_ns); + } + + if (!aBuf.IsEmpty()) { + // This leaves the caret at the right place to insert a bottom signature. + if (aHTMLEditor) { + nsAutoString body(aBuf); + remove_plaintext_tag(body); + htmlEditor->InsertAsCitedQuotation(body, mCiteReference, true, + getter_AddRefs(nodeInserted)); + } else { + htmlEditor->InsertAsQuotation(aBuf, getter_AddRefs(nodeInserted)); + } + } + + (void)TagEmbeddedObjects(htmlEditor); + + if (!aSignature.IsEmpty()) { + // we cannot add it on top earlier, because TagEmbeddedObjects will mark + // all images in the signature as "moz-do-not-send" + if (sigOnTop) MoveToBeginningOfDocument(); + + if (aHTMLEditor) { + bool oldAllow; + GetAllowRemoteContent(&oldAllow); + SetAllowRemoteContent(true); + htmlEditor->InsertHTML(aSignature); + SetAllowRemoteContent(oldAllow); + } else { + htmlEditor->InsertLineBreak(); + InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns); + } + + if (sigOnTop) htmlEditor->EndOfDocument(); + } + } else { + if (aHTMLEditor) { + if (isForwarded && + Substring(aBuf, 0, sizeof(MIME_FORWARD_HTML_PREFIX) - 1) + .EqualsLiteral(MIME_FORWARD_HTML_PREFIX)) { + // We assign the opening tag inside "<HTML><BODY><BR><BR>" before the + // two <br> elements. + // This is a bit hacky but we know that the MIME code prepares the + // forwarded content like this: + // <HTML><BODY><BR><BR> + forwarded header + header table. + // Note: We only do this when we prepare the message to be forwarded, + // a re-opened saved draft of a forwarded message does not repeat this. + nsString divTag; + divTag.AssignLiteral("<div class=\"moz-forward-container\">"); + aBuf.Insert(divTag, sizeof(MIME_FORWARD_HTML_PREFIX) - 1 - 8); + } + remove_plaintext_tag(aBuf); + + bool stripConditionalCSS = mozilla::Preferences::GetBool( + "mail.html_sanitize.drop_conditional_css", true); + + if (stripConditionalCSS) { + nsString newBody; + remove_conditional_CSS(aBuf, newBody); + htmlEditor->RebuildDocumentFromSource(newBody); + } else { + htmlEditor->RebuildDocumentFromSource(aBuf); + } + + // When forwarding a message as inline, or editing as new (which could + // contain unsanitized remote content), tag any embedded objects + // with moz-do-not-send=true so they don't get attached upon send. + if (isForwarded || mType == nsIMsgCompType::EditAsNew) + (void)TagEmbeddedObjects(htmlEditor); + + if (!aSignature.IsEmpty()) { + if (isForwarded && sigOnTop) { + // Use our own function, nsEditor::BeginningOfDocument() would + // position into the <div class="moz-forward-container"> we've just + // created. + MoveToBeginningOfDocument(); + } else { + // Use our own function, nsEditor::EndOfDocument() would position + // into the <div class="moz-forward-container"> we've just created. + MoveToEndOfDocument(); + } + + bool oldAllow; + GetAllowRemoteContent(&oldAllow); + SetAllowRemoteContent(true); + htmlEditor->InsertHTML(aSignature); + SetAllowRemoteContent(oldAllow); + + if (isForwarded && sigOnTop) htmlEditor->EndOfDocument(); + } else + htmlEditor->EndOfDocument(); + } else { + bool sigOnTopInserted = false; + if (isForwarded && sigOnTop && !aSignature.IsEmpty()) { + htmlEditor->InsertLineBreak(); + InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns); + htmlEditor->EndOfDocument(); + sigOnTopInserted = true; + } + + if (!aBuf.IsEmpty()) { + nsresult rv; + RefPtr<Element> divElem; + RefPtr<Element> extraBr; + + if (isForwarded) { + // Special treatment for forwarded messages: Part 1. + // Create a <div> of the required class. + rv = htmlEditor->CreateElementWithDefaults(u"div"_ns, + getter_AddRefs(divElem)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString attributeName; + nsAutoString attributeValue; + attributeName.AssignLiteral("class"); + attributeValue.AssignLiteral("moz-forward-container"); + IgnoredErrorResult rv1; + divElem->SetAttribute(attributeName, attributeValue, rv1); + + // We can't insert an empty <div>, so fill it with something. + rv = htmlEditor->CreateElementWithDefaults(u"br"_ns, + getter_AddRefs(extraBr)); + NS_ENSURE_SUCCESS(rv, rv); + + ErrorResult rv2; + divElem->AppendChild(*extraBr, rv2); + if (rv2.Failed()) { + return rv2.StealNSResult(); + } + + // Insert the non-empty <div> into the DOM. + rv = htmlEditor->InsertElementAtSelection(divElem, false); + NS_ENSURE_SUCCESS(rv, rv); + + // Position into the div, so out content goes there. + RefPtr<Selection> selection; + htmlEditor->GetSelection(getter_AddRefs(selection)); + rv = selection->CollapseInLimiter(divElem, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = htmlEditor->InsertTextWithQuotations(aBuf); + NS_ENSURE_SUCCESS(rv, rv); + + if (isForwarded) { + // Special treatment for forwarded messages: Part 2. + if (sigOnTopInserted) { + // Sadly the M-C editor inserts a <br> between the <div> for the + // signature and this <div>, so remove the <br> we don't want. + nsCOMPtr<nsINode> brBeforeDiv; + nsAutoString tagLocalName; + brBeforeDiv = divElem->GetPreviousSibling(); + if (brBeforeDiv) { + tagLocalName = brBeforeDiv->LocalName(); + if (tagLocalName.EqualsLiteral("br")) { + rv = htmlEditor->DeleteNode(brBeforeDiv); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // Clean up the <br> we inserted. + rv = htmlEditor->DeleteNode(extraBr); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Use our own function instead of nsEditor::EndOfDocument() because + // we don't want to position at the end of the div we've just created. + // It's OK to use, even if we're not forwarding and didn't create a + // <div>. + rv = MoveToEndOfDocument(); + NS_ENSURE_SUCCESS(rv, rv); + } + + if ((!isForwarded || !sigOnTop) && !aSignature.IsEmpty()) { + htmlEditor->InsertLineBreak(); + InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns); + } + } + } + + if (aBuf.IsEmpty()) + htmlEditor->BeginningOfDocument(); + else { + switch (reply_on_top) { + // This should set the cursor after the body but before the sig + case 0: { + if (!htmlEditor) { + htmlEditor->BeginningOfDocument(); + break; + } + + RefPtr<Selection> selection; + nsCOMPtr<nsINode> parent; + int32_t offset; + nsresult rv; + + // get parent and offset of mailcite + rv = GetNodeLocation(nodeInserted, address_of(parent), &offset); + if (NS_FAILED(rv) || (!parent)) { + htmlEditor->BeginningOfDocument(); + break; + } + + // get selection + htmlEditor->GetSelection(getter_AddRefs(selection)); + if (!selection) { + htmlEditor->BeginningOfDocument(); + break; + } + + // place selection after mailcite + selection->CollapseInLimiter(parent, offset + 1); + + // insert a break at current selection + if (!paragraphMode || !aHTMLEditor) htmlEditor->InsertLineBreak(); + + // i'm not sure if you need to move the selection back to before the + // break. expirement. + selection->CollapseInLimiter(parent, offset + 1); + + break; + } + + case 2: { + nsCOMPtr<nsIEditor> editor(htmlEditor); // Strong reference. + editor->SelectAll(); + break; + } + + // This should set the cursor to the top! + default: { + MoveToBeginningOfDocument(); + break; + } + } + } + + nsCOMPtr<nsISelectionController> selCon; + htmlEditor->GetSelectionController(getter_AddRefs(selCon)); + + if (selCon) + selCon->ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_ANCHOR_REGION, true); + + htmlEditor->EnableUndo(true); + SetBodyModified(false); + +#ifdef MSGCOMP_TRACE_PERFORMANCE + nsCOMPtr<nsIMsgComposeService> composeService( + do_GetService("@mozilla.org/messengercompose;1")); + composeService->TimeStamp( + "Finished inserting data into the editor. The window is finally ready!", + false); +#endif + return NS_OK; +} + +/** + * Check the identity pref to include signature on replies and forwards. + */ +bool nsMsgCompose::CheckIncludeSignaturePrefs(nsIMsgIdentity* identity) { + bool includeSignature = true; + switch (mType) { + case nsIMsgCompType::ForwardInline: + case nsIMsgCompType::ForwardAsAttachment: + identity->GetSigOnForward(&includeSignature); + break; + case nsIMsgCompType::Reply: + case nsIMsgCompType::ReplyAll: + case nsIMsgCompType::ReplyToList: + case nsIMsgCompType::ReplyToGroup: + case nsIMsgCompType::ReplyToSender: + case nsIMsgCompType::ReplyToSenderAndGroup: + identity->GetSigOnReply(&includeSignature); + break; + } + return includeSignature; +} + +nsresult nsMsgCompose::SetQuotingToFollow(bool aVal) { + mQuotingToFollow = aVal; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::GetQuotingToFollow(bool* quotingToFollow) { + NS_ENSURE_ARG(quotingToFollow); + *quotingToFollow = mQuotingToFollow; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::Initialize(nsIMsgComposeParams* aParams, + mozIDOMWindowProxy* aWindow, nsIDocShell* aDocShell) { + NS_ENSURE_ARG_POINTER(aParams); + nsresult rv; + + aParams->GetIdentity(getter_AddRefs(m_identity)); + + if (aWindow) { + m_window = aWindow; + nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDocShellTreeItem> treeItem = window->GetDocShell(); + nsCOMPtr<nsIDocShellTreeOwner> treeOwner; + rv = treeItem->GetTreeOwner(getter_AddRefs(treeOwner)); + if (NS_FAILED(rv)) return rv; + + m_baseWindow = do_QueryInterface(treeOwner); + } + + aParams->GetAutodetectCharset(&mAutodetectCharset); + + MSG_ComposeFormat format; + aParams->GetFormat(&format); + + MSG_ComposeType type; + aParams->GetType(&type); + + nsCString originalMsgURI; + aParams->GetOriginalMsgURI(originalMsgURI); + aParams->GetOrigMsgHdr(getter_AddRefs(mOrigMsgHdr)); + + nsCOMPtr<nsIMsgCompFields> composeFields; + aParams->GetComposeFields(getter_AddRefs(composeFields)); + + nsCOMPtr<nsIMsgComposeService> composeService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = composeService->DetermineComposeHTML(m_identity, format, &m_composeHTML); + NS_ENSURE_SUCCESS(rv, rv); + +#ifndef MOZ_SUITE + if (m_composeHTML) { + Telemetry::ScalarAdd(Telemetry::ScalarID::TB_COMPOSE_FORMAT_HTML, 1); + } else { + Telemetry::ScalarAdd(Telemetry::ScalarID::TB_COMPOSE_FORMAT_PLAIN_TEXT, 1); + } + Telemetry::Accumulate(Telemetry::TB_COMPOSE_TYPE, type); +#endif + + if (composeFields) { + nsAutoCString draftId; // will get set for drafts and templates + rv = composeFields->GetDraftId(draftId); + NS_ENSURE_SUCCESS(rv, rv); + + // Set return receipt flag and type, and if we should attach a vCard + // by checking the identity prefs - but don't clobber the values for + // drafts and templates as they were set up already by mime when + // initializing the message. + if (m_identity && draftId.IsEmpty() && type != nsIMsgCompType::Template) { + bool requestReturnReceipt = false; + rv = m_identity->GetRequestReturnReceipt(&requestReturnReceipt); + NS_ENSURE_SUCCESS(rv, rv); + rv = composeFields->SetReturnReceipt(requestReturnReceipt); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t receiptType = nsIMsgMdnGenerator::eDntType; + rv = m_identity->GetReceiptHeaderType(&receiptType); + NS_ENSURE_SUCCESS(rv, rv); + rv = composeFields->SetReceiptHeaderType(receiptType); + NS_ENSURE_SUCCESS(rv, rv); + + bool requestDSN = false; + rv = m_identity->GetRequestDSN(&requestDSN); + NS_ENSURE_SUCCESS(rv, rv); + rv = composeFields->SetDSN(requestDSN); + NS_ENSURE_SUCCESS(rv, rv); + + bool attachVCard; + rv = m_identity->GetAttachVCard(&attachVCard); + NS_ENSURE_SUCCESS(rv, rv); + rv = composeFields->SetAttachVCard(attachVCard); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + nsCOMPtr<nsIMsgSendListener> externalSendListener; + aParams->GetSendListener(getter_AddRefs(externalSendListener)); + if (externalSendListener) AddMsgSendListener(externalSendListener); + + nsString smtpPassword; + aParams->GetSmtpPassword(smtpPassword); + mSmtpPassword = smtpPassword; + + aParams->GetHtmlToQuote(mHtmlToQuote); + + if (aDocShell) { + mDocShell = aDocShell; + // register the compose object with the compose service + rv = composeService->RegisterComposeDocShell(aDocShell, this); + NS_ENSURE_SUCCESS(rv, rv); + } + return CreateMessage(originalMsgURI, type, composeFields); +} + +NS_IMETHODIMP +nsMsgCompose::RegisterStateListener( + nsIMsgComposeStateListener* aStateListener) { + NS_ENSURE_ARG_POINTER(aStateListener); + mStateListeners.AppendElement(aStateListener); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::UnregisterStateListener( + nsIMsgComposeStateListener* aStateListener) { + NS_ENSURE_ARG_POINTER(aStateListener); + return mStateListeners.RemoveElement(aStateListener) ? NS_OK + : NS_ERROR_FAILURE; +} + +// Added to allow easier use of the nsIMsgSendListener +NS_IMETHODIMP nsMsgCompose::AddMsgSendListener( + nsIMsgSendListener* aMsgSendListener) { + NS_ENSURE_ARG_POINTER(aMsgSendListener); + mExternalSendListeners.AppendElement(aMsgSendListener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::RemoveMsgSendListener( + nsIMsgSendListener* aMsgSendListener) { + NS_ENSURE_ARG_POINTER(aMsgSendListener); + return mExternalSendListeners.RemoveElement(aMsgSendListener) + ? NS_OK + : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMsgCompose::SendMsgToServer(MSG_DeliverMode deliverMode, + nsIMsgIdentity* identity, const char* accountKey, + Promise** aPromise) { + nsresult rv = NS_OK; + + // clear saved message id if sending, so we don't send out the same + // message-id. + if (deliverMode == nsIMsgCompDeliverMode::Now || + deliverMode == nsIMsgCompDeliverMode::Later || + deliverMode == nsIMsgCompDeliverMode::Background) + m_compFields->SetMessageId(""); + + if (m_compFields && identity) { + // Pref values are supposed to be stored as UTF-8, so no conversion + nsCString email; + nsString fullName; + nsString organization; + + identity->GetEmail(email); + identity->GetFullName(fullName); + identity->GetOrganization(organization); + + const char* pFrom = m_compFields->GetFrom(); + if (!pFrom || !*pFrom) { + nsCString sender; + MakeMimeAddress(NS_ConvertUTF16toUTF8(fullName), email, sender); + m_compFields->SetFrom(sender.IsEmpty() ? email.get() : sender.get()); + } + + m_compFields->SetOrganization(organization); + + // We need an nsIMsgSend instance to send the message. Allow extensions + // to override the default SMTP sender by observing mail-set-sender. + mMsgSend = nullptr; + mDeliverMode = deliverMode; // save for possible access by observer. + + // Allow extensions to specify an outgoing server. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_STATE(observerService); + + // Assemble a string with sending parameters. + nsAutoString sendParms; + + // First parameter: account key. This may be null. + sendParms.AppendASCII(accountKey && *accountKey ? accountKey : ""); + sendParms.Append(','); + + // Second parameter: deliverMode. + sendParms.AppendInt(deliverMode); + sendParms.Append(','); + + // Third parameter: identity (as identity key). + nsAutoCString identityKey; + identity->GetKey(identityKey); + sendParms.AppendASCII(identityKey.get()); + + observerService->NotifyObservers(NS_ISUPPORTS_CAST(nsIMsgCompose*, this), + "mail-set-sender", sendParms.get()); + + if (!mMsgSend) + mMsgSend = do_CreateInstance("@mozilla.org/messengercompose/send;1"); + + if (mMsgSend) { + nsString bodyString; + rv = m_compFields->GetBody(bodyString); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the listener for the send operation... + nsCOMPtr<nsIMsgComposeSendListener> composeSendListener = + do_CreateInstance( + "@mozilla.org/messengercompose/composesendlistener;1"); + if (!composeSendListener) return NS_ERROR_OUT_OF_MEMORY; + + // right now, AutoSaveAsDraft is identical to SaveAsDraft as + // far as the msg send code is concerned. This way, we don't have + // to add an nsMsgDeliverMode for autosaveasdraft, and add cases for + // it in the msg send code. + if (deliverMode == nsIMsgCompDeliverMode::AutoSaveAsDraft) + deliverMode = nsIMsgCompDeliverMode::SaveAsDraft; + + RefPtr<nsIMsgCompose> msgCompose(this); + composeSendListener->SetMsgCompose(msgCompose); + composeSendListener->SetDeliverMode(deliverMode); + + if (mProgress) { + nsCOMPtr<nsIWebProgressListener> progressListener = + do_QueryInterface(composeSendListener); + mProgress->RegisterListener(progressListener); + } + + // If we are composing HTML, then this should be sent as + // multipart/related which means we pass the editor into the + // backend...if not, just pass nullptr + // + nsCOMPtr<nsIMsgSendListener> sendListener = + do_QueryInterface(composeSendListener); + RefPtr<mozilla::dom::Promise> promise; + rv = mMsgSend->CreateAndSendMessage( + m_composeHTML ? m_editor.get() : nullptr, identity, accountKey, + m_compFields, false, false, (nsMsgDeliverMode)deliverMode, nullptr, + m_composeHTML ? TEXT_HTML : TEXT_PLAIN, bodyString, m_window, + mProgress, sendListener, mSmtpPassword, mOriginalMsgURI, mType, + getter_AddRefs(promise)); + promise.forget(aPromise); + } else + rv = NS_ERROR_FAILURE; + } else + rv = NS_ERROR_NOT_INITIALIZED; + + return rv; +} + +NS_IMETHODIMP nsMsgCompose::SendMsg(MSG_DeliverMode deliverMode, + nsIMsgIdentity* identity, + const char* accountKey, + nsIMsgWindow* aMsgWindow, + nsIMsgProgress* progress, + Promise** aPromise) { + NS_ENSURE_TRUE(m_compFields, NS_ERROR_NOT_INITIALIZED); + nsresult rv = NS_OK; + + // Set content type based on which type of compose window we had. + nsString contentType = (m_composeHTML) ? u"text/html"_ns : u"text/plain"_ns; + nsString msgBody; + if (m_editor) { + // Reset message body previously stored in the compose fields + m_compFields->SetBody(EmptyString()); + + uint32_t flags = nsIDocumentEncoder::OutputCRLineBreak | + nsIDocumentEncoder::OutputLFLineBreak; + + if (m_composeHTML) { + flags |= nsIDocumentEncoder::OutputFormatted | + nsIDocumentEncoder::OutputDisallowLineBreaking; + } else { + bool flowed, formatted; + GetSerialiserFlags(&flowed, &formatted); + if (flowed) flags |= nsIDocumentEncoder::OutputFormatFlowed; + if (formatted) flags |= nsIDocumentEncoder::OutputFormatted; + flags |= nsIDocumentEncoder::OutputDisallowLineBreaking; + // Don't lose NBSP in the plain text encoder. + flags |= nsIDocumentEncoder::OutputPersistNBSP; + } + nsresult rv = m_editor->OutputToString(contentType, flags, msgBody); + NS_ENSURE_SUCCESS(rv, rv); + } else { + m_compFields->GetBody(msgBody); + } + if (!msgBody.IsEmpty()) { + // Ensure body ends in CRLF to avoid SMTP server timeout when sent. + if (!StringEndsWith(msgBody, u"\r\n"_ns)) msgBody.AppendLiteral("\r\n"); + bool isAsciiOnly = mozilla::IsAsciiNullTerminated( + static_cast<const char16_t*>(msgBody.get())); + + if (m_compFields->GetForceMsgEncoding()) { + isAsciiOnly = false; + } + + m_compFields->SetBodyIsAsciiOnly(isAsciiOnly); + m_compFields->SetBody(msgBody); + } + + // Let's open the progress dialog + if (progress) { + mProgress = progress; + + if (deliverMode != nsIMsgCompDeliverMode::AutoSaveAsDraft) { + nsAutoString msgSubject; + m_compFields->GetSubject(msgSubject); + + bool showProgress = false; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) { + prefBranch->GetBoolPref("mailnews.show_send_progress", &showProgress); + if (showProgress) { + nsCOMPtr<nsIMsgComposeProgressParams> params = do_CreateInstance( + "@mozilla.org/messengercompose/composeprogressparameters;1", &rv); + if (NS_FAILED(rv) || !params) return NS_ERROR_FAILURE; + + params->SetSubject(msgSubject.get()); + params->SetDeliveryMode(deliverMode); + + mProgress->OpenProgressDialog( + m_window, aMsgWindow, + "chrome://messenger/content/messengercompose/sendProgress.xhtml", + false, params); + } + } + } + + mProgress->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_START, NS_OK); + } + + bool attachVCard = false; + m_compFields->GetAttachVCard(&attachVCard); + + if (attachVCard && identity && + (deliverMode == nsIMsgCompDeliverMode::Now || + deliverMode == nsIMsgCompDeliverMode::Later || + deliverMode == nsIMsgCompDeliverMode::Background)) { + nsCString escapedVCard; + // make sure, if there is no card, this returns an empty string, or + // NS_ERROR_FAILURE + rv = identity->GetEscapedVCard(escapedVCard); + + if (NS_SUCCEEDED(rv) && !escapedVCard.IsEmpty()) { + nsCString vCardUrl; + vCardUrl = "data:text/vcard;charset=utf-8;base64,"; + nsCString unescapedData; + MsgUnescapeString(escapedVCard, 0, unescapedData); + char* result = PL_Base64Encode(unescapedData.get(), 0, nullptr); + vCardUrl += result; + PR_Free(result); + + nsCOMPtr<nsIMsgAttachment> attachment = + do_CreateInstance("@mozilla.org/messengercompose/attachment;1", &rv); + if (NS_SUCCEEDED(rv) && attachment) { + // [comment from 4.x] + // Send the vCard out with a filename which distinguishes this user. + // e.g. jsmith.vcf The main reason to do this is for interop with + // Eudora, which saves off the attachments separately from the message + // body + nsCString userid; + (void)identity->GetEmail(userid); + int32_t index = userid.FindChar('@'); + if (index != kNotFound) userid.SetLength(index); + + if (userid.IsEmpty()) + attachment->SetName(u"vcard.vcf"_ns); + else { + // Replace any dot with underscore to stop vCards + // generating false positives with some heuristic scanners + userid.ReplaceChar('.', '_'); + userid.AppendLiteral(".vcf"); + attachment->SetName(NS_ConvertASCIItoUTF16(userid)); + } + + attachment->SetUrl(vCardUrl); + m_compFields->AddAttachment(attachment); + } + } + } + + // Save the identity being sent for later use. + m_identity = identity; + + RefPtr<mozilla::dom::Promise> promise; + rv = SendMsgToServer(deliverMode, identity, accountKey, + getter_AddRefs(promise)); + + RefPtr<nsMsgCompose> self = this; + auto handleFailure = [self = std::move(self), deliverMode](nsresult rv) { + self->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeProcessDone, rv); + nsCOMPtr<nsIMsgSendReport> sendReport; + if (self->mMsgSend) + self->mMsgSend->GetSendReport(getter_AddRefs(sendReport)); + if (sendReport) { + nsresult theError; + sendReport->DisplayReport(self->m_window, true, true, &theError); + } else { + // If we come here it's because we got an error before we could initialize + // a send report! Let's try our best... + switch (deliverMode) { + case nsIMsgCompDeliverMode::Later: + nsMsgDisplayMessageByName(self->m_window, "unableToSendLater"); + break; + case nsIMsgCompDeliverMode::AutoSaveAsDraft: + case nsIMsgCompDeliverMode::SaveAsDraft: + nsMsgDisplayMessageByName(self->m_window, "unableToSaveDraft"); + break; + case nsIMsgCompDeliverMode::SaveAsTemplate: + nsMsgDisplayMessageByName(self->m_window, "unableToSaveTemplate"); + break; + + default: + nsMsgDisplayMessageByName(self->m_window, "sendFailed"); + break; + } + } + if (self->mProgress) self->mProgress->CloseProgressDialog(true); + + self->DeleteTmpAttachments(); + }; + if (promise) { + RefPtr<DomPromiseListener> listener = new DomPromiseListener( + [&](JSContext*, JS::Handle<JS::Value>) { DeleteTmpAttachments(); }, + handleFailure); + promise->AppendNativeHandler(listener); + promise.forget(aPromise); + } else if (NS_FAILED(rv)) { + handleFailure(rv); + } + + return rv; +} + +NS_IMETHODIMP nsMsgCompose::GetDeleteDraft(bool* aDeleteDraft) { + NS_ENSURE_ARG_POINTER(aDeleteDraft); + *aDeleteDraft = mDeleteDraft; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::SetDeleteDraft(bool aDeleteDraft) { + mDeleteDraft = aDeleteDraft; + return NS_OK; +} + +bool nsMsgCompose::IsLastWindow() { + nsresult rv; + bool more; + nsCOMPtr<nsIWindowMediator> windowMediator = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsISimpleEnumerator> windowEnumerator; + rv = windowMediator->GetEnumerator(nullptr, + getter_AddRefs(windowEnumerator)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsISupports> isupports; + + if (NS_SUCCEEDED(windowEnumerator->GetNext(getter_AddRefs(isupports)))) + if (NS_SUCCEEDED(windowEnumerator->HasMoreElements(&more))) + return !more; + } + } + return true; +} + +NS_IMETHODIMP nsMsgCompose::CloseWindow(void) { + nsresult rv; + + nsCOMPtr<nsIMsgComposeService> composeService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // unregister the compose object with the compose service + rv = composeService->UnregisterComposeDocShell(mDocShell); + NS_ENSURE_SUCCESS(rv, rv); + mDocShell = nullptr; + + // ensure that the destructor of nsMsgSend is invoked to remove + // temporary files. + mMsgSend = nullptr; + + // We are going away for real, we need to do some clean up first + if (m_baseWindow) { + if (m_editor) { + // The editor will be destroyed during the close window. + // Set it to null to be sure we won't use it anymore. + m_editor = nullptr; + } + nsCOMPtr<nsIBaseWindow> window = m_baseWindow.forget(); + rv = window->Destroy(); + } + + m_window = nullptr; + return rv; +} + +nsresult nsMsgCompose::Abort() { + if (mMsgSend) mMsgSend->Abort(); + + if (mProgress) mProgress->CloseProgressDialog(true); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetEditor(nsIEditor** aEditor) { + NS_IF_ADDREF(*aEditor = m_editor); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::SetEditor(nsIEditor* aEditor) { + m_editor = aEditor; + return NS_OK; +} + +// This used to be called BEFORE editor was created +// (it did the loadURI that triggered editor creation) +// It is called from JS after editor creation +// (loadURI is done in JS) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsMsgCompose::InitEditor( + nsIEditor* aEditor, mozIDOMWindowProxy* aContentWindow) { + NS_ENSURE_ARG_POINTER(aEditor); + NS_ENSURE_ARG_POINTER(aContentWindow); + nsresult rv; + + m_editor = aEditor; + + aEditor->SetDocumentCharacterSet("UTF-8"_ns); + + nsCOMPtr<nsPIDOMWindowOuter> window = + nsPIDOMWindowOuter::From(aContentWindow); + + nsIDocShell* docShell = window->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_UNEXPECTED); + + bool quotingToFollow = false; + GetQuotingToFollow("ingToFollow); + if (quotingToFollow) + return BuildQuotedMessageAndSignature(); + else { + NotifyStateListeners(nsIMsgComposeNotificationType::ComposeFieldsReady, + NS_OK); + rv = BuildBodyMessageAndSignature(); + NotifyStateListeners(nsIMsgComposeNotificationType::ComposeBodyReady, + NS_OK); + return rv; + } +} + +nsresult nsMsgCompose::GetBodyModified(bool* modified) { + nsresult rv; + + if (!modified) return NS_ERROR_NULL_POINTER; + + *modified = true; + + if (m_editor) { + rv = m_editor->GetDocumentModified(modified); + if (NS_FAILED(rv)) *modified = true; + } + + return NS_OK; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult +nsMsgCompose::SetBodyModified(bool modified) { + nsresult rv = NS_OK; + + if (m_editor) { + nsCOMPtr<nsIEditor> editor(m_editor); // Strong reference. + if (modified) { + int32_t modCount = 0; + editor->GetModificationCount(&modCount); + if (modCount == 0) editor->IncrementModificationCount(1); + } else + editor->ResetModificationCount(); + } + + return rv; +} + +NS_IMETHODIMP +nsMsgCompose::GetDomWindow(mozIDOMWindowProxy** aDomWindow) { + NS_IF_ADDREF(*aDomWindow = m_window); + return NS_OK; +} + +nsresult nsMsgCompose::GetCompFields(nsIMsgCompFields** aCompFields) { + NS_IF_ADDREF(*aCompFields = (nsIMsgCompFields*)m_compFields); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetComposeHTML(bool* aComposeHTML) { + *aComposeHTML = m_composeHTML; + return NS_OK; +} + +nsresult nsMsgCompose::GetWrapLength(int32_t* aWrapLength) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + + return prefBranch->GetIntPref("mailnews.wraplength", aWrapLength); +} + +nsresult nsMsgCompose::CreateMessage(const nsACString& originalMsgURI, + MSG_ComposeType type, + nsIMsgCompFields* compFields) { + nsresult rv = NS_OK; + mType = type; + mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_None; + + mDeleteDraft = (type == nsIMsgCompType::Draft); + nsAutoCString msgUri(originalMsgURI); + bool fileUrl = StringBeginsWith(msgUri, "file:"_ns); + int32_t typeIndex = msgUri.Find("type=application/x-message-display"); + if (typeIndex != kNotFound && typeIndex > 0) { + // Strip out type=application/x-message-display because it confuses libmime. + msgUri.Cut(typeIndex, sizeof("type=application/x-message-display")); + if (fileUrl) // we're dealing with an .eml file msg + { + // We have now removed the type from the uri. Make sure we don't have + // an uri with "&&" now. If we do, remove the second '&'. + if (msgUri.CharAt(typeIndex) == '&') msgUri.Cut(typeIndex, 1); + // Remove possible trailing '?'. + if (msgUri.CharAt(msgUri.Length() - 1) == '?') + msgUri.Cut(msgUri.Length() - 1, 1); + } else // we're dealing with a message/rfc822 attachment + { + // nsURLFetcher will check for "realtype=message/rfc822" and will set the + // content type to message/rfc822 in the forwarded message. + msgUri.AppendLiteral("&realtype=message/rfc822"); + } + } + + if (compFields) { + m_compFields = reinterpret_cast<nsMsgCompFields*>(compFields); + } else { + m_compFields = new nsMsgCompFields(); + } + + if (m_identity && mType != nsIMsgCompType::Draft) { + // Setup reply-to field. + nsCString replyTo; + m_identity->GetReplyTo(replyTo); + if (!replyTo.IsEmpty()) { + nsCString resultStr; + RemoveDuplicateAddresses(nsDependentCString(m_compFields->GetReplyTo()), + replyTo, resultStr); + if (!resultStr.IsEmpty()) { + replyTo.Append(','); + replyTo.Append(resultStr); + } + m_compFields->SetReplyTo(replyTo.get()); + } + + // Setup auto-Cc field. + bool doCc; + m_identity->GetDoCc(&doCc); + if (doCc) { + nsCString ccList; + m_identity->GetDoCcList(ccList); + + nsCString resultStr; + RemoveDuplicateAddresses(nsDependentCString(m_compFields->GetCc()), + ccList, resultStr); + if (!resultStr.IsEmpty()) { + ccList.Append(','); + ccList.Append(resultStr); + } + m_compFields->SetCc(ccList.get()); + } + + // Setup auto-Bcc field. + bool doBcc; + m_identity->GetDoBcc(&doBcc); + if (doBcc) { + nsCString bccList; + m_identity->GetDoBccList(bccList); + + nsCString resultStr; + RemoveDuplicateAddresses(nsDependentCString(m_compFields->GetBcc()), + bccList, resultStr); + if (!resultStr.IsEmpty()) { + bccList.Append(','); + bccList.Append(resultStr); + } + m_compFields->SetBcc(bccList.get()); + } + } + + if (mType == nsIMsgCompType::Draft) { + nsCString curDraftIdURL; + rv = m_compFields->GetDraftId(curDraftIdURL); + // Skip if no draft id (probably a new draft msg). + if (NS_SUCCEEDED(rv) && !curDraftIdURL.IsEmpty()) { + nsCOMPtr<nsIMsgDBHdr> msgDBHdr; + rv = GetMsgDBHdrFromURI(curDraftIdURL, getter_AddRefs(msgDBHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv), + "CreateMessage can't get msg header DB interface pointer."); + if (msgDBHdr) { + nsCString queuedDisposition; + msgDBHdr->GetStringProperty(QUEUED_DISPOSITION_PROPERTY, + queuedDisposition); + // We need to retrieve the original URI from the database so we can + // set the disposition flags correctly if the draft is a reply or + // forwarded message. + nsCString originalMsgURIfromDB; + msgDBHdr->GetStringProperty(ORIG_URI_PROPERTY, originalMsgURIfromDB); + mOriginalMsgURI = originalMsgURIfromDB; + if (!queuedDisposition.IsEmpty()) { + if (queuedDisposition.EqualsLiteral("replied")) + mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_Replied; + else if (queuedDisposition.EqualsLiteral("forward")) + mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_Forwarded; + else if (queuedDisposition.EqualsLiteral("redirected")) + mDraftDisposition = nsIMsgFolder::nsMsgDispositionState_Redirected; + } + } + } else { + NS_WARNING("CreateMessage can't get draft id"); + } + } + + // If we don't have an original message URI, nothing else to do... + if (msgUri.IsEmpty()) return NS_OK; + + // store the original message URI so we can extract it after we send the + // message to properly mark any disposition flags like replied or forwarded on + // the message. + if (mOriginalMsgURI.IsEmpty()) mOriginalMsgURI = msgUri; + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // "Forward inline" and "Reply with template" processing. + // Note the early return at the end of the block. + if (type == nsIMsgCompType::ForwardInline || + type == nsIMsgCompType::ReplyWithTemplate) { + // We want to treat this message as a reference too + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(msgUri, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + + nsAutoCString reference; + // When forwarding we only use the original message for "References:" - + // recipients don't have the other messages anyway. + // For reply with template we want to preserve all the references. + if (type == nsIMsgCompType::ReplyWithTemplate) { + uint16_t numReferences = 0; + msgHdr->GetNumReferences(&numReferences); + for (int32_t i = 0; i < numReferences; i++) { + nsAutoCString ref; + msgHdr->GetStringReference(i, ref); + if (!ref.IsEmpty()) { + reference.Append('<'); + reference.Append(ref); + reference.AppendLiteral("> "); + } + } + reference.Trim(" ", false, true); + } + msgHdr->GetMessageId(getter_Copies(messageId)); + reference.Append('<'); + reference.Append(messageId); + reference.Append('>'); + m_compFields->SetReferences(reference.get()); + + if (type == nsIMsgCompType::ForwardInline) { + nsString subject; + msgHdr->GetMime2DecodedSubject(subject); + nsCString fwdPrefix; + prefs->GetCharPrefWithDefault("mail.forward_subject_prefix", "Fwd"_ns, + 1, fwdPrefix); + nsString unicodeFwdPrefix; + CopyUTF8toUTF16(fwdPrefix, unicodeFwdPrefix); + unicodeFwdPrefix.AppendLiteral(": "); + subject.Insert(unicodeFwdPrefix, 0); + m_compFields->SetSubject(subject); + } + } + + // Early return for "ForwardInline" and "ReplyWithTemplate" processing. + return NS_OK; + } + + // All other processing. + + // Note the following: + // LoadDraftOrTemplate() is run in nsMsgComposeService::OpenComposeWindow() + // for five compose types: ForwardInline, ReplyWithTemplate (both covered + // in the code block above) and Draft, Template and Redirect. For these + // compose types, the charset is already correct (incl. MIME-applied override) + // unless the default charset should be used. + + bool isFirstPass = true; + char* uriList = ToNewCString(msgUri); + char* uri = uriList; + char* nextUri; + do { + nextUri = strstr(uri, "://"); + if (nextUri) { + // look for next ://, and then back up to previous ',' + nextUri = strstr(nextUri + 1, "://"); + if (nextUri) { + *nextUri = '\0'; + char* saveNextUri = nextUri; + nextUri = strrchr(uri, ','); + if (nextUri) *nextUri = '\0'; + *saveNextUri = ':'; + } + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + if (mOrigMsgHdr) + msgHdr = mOrigMsgHdr; + else { + rv = GetMsgDBHdrFromURI(nsDependentCString(uri), getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + } + if (msgHdr) { + nsString subject; + rv = msgHdr->GetMime2DecodedSubject(subject); + if (NS_FAILED(rv)) return rv; + + // Check if (was: is present in the subject + int32_t wasOffset = subject.RFind(u" (was:"_ns); + bool strip = true; + + if (wasOffset >= 0) { + // Check the number of references, to check if was: should be stripped + // First, assume that it should be stripped; the variable will be set to + // false later if stripping should not happen. + uint16_t numRef; + msgHdr->GetNumReferences(&numRef); + if (numRef) { + // If there are references, look for the first message in the thread + // firstly, get the database via the folder + nsCOMPtr<nsIMsgFolder> folder; + msgHdr->GetFolder(getter_AddRefs(folder)); + if (folder) { + nsCOMPtr<nsIMsgDatabase> db; + folder->GetMsgDatabase(getter_AddRefs(db)); + + if (db) { + nsAutoCString reference; + msgHdr->GetStringReference(0, reference); + + nsCOMPtr<nsIMsgDBHdr> refHdr; + db->GetMsgHdrForMessageID(reference.get(), + getter_AddRefs(refHdr)); + + if (refHdr) { + nsCString refSubject; + rv = refHdr->GetSubject(refSubject); + if (NS_SUCCEEDED(rv)) { + if (refSubject.Find(" (was:") >= 0) strip = false; + } + } + } + } + } else + strip = false; + } + + if (strip && wasOffset >= 0) { + // Strip off the "(was: old subject)" part + subject.Assign(Substring(subject, 0, wasOffset)); + } + + switch (type) { + default: + break; + case nsIMsgCompType::Draft: + case nsIMsgCompType::Template: + case nsIMsgCompType::EditTemplate: + case nsIMsgCompType::EditAsNew: { + // If opening from file, preseve the subject already present, since + // we can't get a subject from db there. + if (mOriginalMsgURI.Find("&realtype=message/rfc822") != -1) { + break; + } + // Otherwise, set up the subject from db, with possible modifications. + uint32_t flags; + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::HasRe) { + subject.InsertLiteral(u"Re: ", 0); + } + // Set subject from db, where it's already decrypted. The raw + // header may be encrypted. + m_compFields->SetSubject(subject); + break; + } + case nsIMsgCompType::Reply: + case nsIMsgCompType::ReplyAll: + case nsIMsgCompType::ReplyToList: + case nsIMsgCompType::ReplyToGroup: + case nsIMsgCompType::ReplyToSender: + case nsIMsgCompType::ReplyToSenderAndGroup: { + if (!isFirstPass) // safeguard, just in case... + { + PR_Free(uriList); + return rv; + } + mQuotingToFollow = true; + + subject.InsertLiteral(u"Re: ", 0); + m_compFields->SetSubject(subject); + + // Setup quoting callbacks for later... + mWhatHolder = 1; + break; + } + case nsIMsgCompType::ForwardAsAttachment: { + // Add the forwarded message in the references, first + nsAutoCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + if (isFirstPass) { + nsAutoCString reference; + reference.Append('<'); + reference.Append(messageId); + reference.Append('>'); + m_compFields->SetReferences(reference.get()); + } else { + nsAutoCString references; + m_compFields->GetReferences(getter_Copies(references)); + references.AppendLiteral(" <"); + references.Append(messageId); + references.Append('>'); + m_compFields->SetReferences(references.get()); + } + + uint32_t flags; + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::HasRe) + subject.InsertLiteral(u"Re: ", 0); + + // Setup quoting callbacks for later... + mQuotingToFollow = + false; // We don't need to quote the original message. + nsCOMPtr<nsIMsgAttachment> attachment = do_CreateInstance( + "@mozilla.org/messengercompose/attachment;1", &rv); + if (NS_SUCCEEDED(rv) && attachment) { + bool addExtension = true; + nsString sanitizedSubj; + prefs->GetBoolPref("mail.forward_add_extension", &addExtension); + + // copy subject string to sanitizedSubj, use default if empty + if (subject.IsEmpty()) { + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> composeBundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/messengercompose/" + "composeMsgs.properties", + getter_AddRefs(composeBundle)); + NS_ENSURE_SUCCESS(rv, rv); + composeBundle->GetStringFromName("messageAttachmentSafeName", + sanitizedSubj); + } else + sanitizedSubj.Assign(subject); + + // set the file size + uint32_t messageSize; + msgHdr->GetMessageSize(&messageSize); + attachment->SetSize(messageSize); + + // change all '.' to '_' see bug #271211 + sanitizedSubj.ReplaceChar(u".", u'_'); + if (addExtension) sanitizedSubj.AppendLiteral(".eml"); + attachment->SetName(sanitizedSubj); + attachment->SetUrl(nsDependentCString(uri)); + m_compFields->AddAttachment(attachment); + } + + if (isFirstPass) { + nsCString fwdPrefix; + prefs->GetCharPrefWithDefault("mail.forward_subject_prefix", + "Fwd"_ns, 1, fwdPrefix); + nsString unicodeFwdPrefix; + CopyUTF8toUTF16(fwdPrefix, unicodeFwdPrefix); + unicodeFwdPrefix.AppendLiteral(": "); + subject.Insert(unicodeFwdPrefix, 0); + m_compFields->SetSubject(subject); + } + break; + } + case nsIMsgCompType::Redirect: { + // For a redirect, set the Reply-To: header to what was in the + // original From: header... + nsAutoCString author; + msgHdr->GetAuthor(getter_Copies(author)); + m_compFields->SetSubject(subject); + m_compFields->SetReplyTo(author.get()); + + // ... and empty out the various recipient headers + nsAutoString empty; + m_compFields->SetTo(empty); + m_compFields->SetCc(empty); + m_compFields->SetBcc(empty); + m_compFields->SetNewsgroups(empty); + m_compFields->SetFollowupTo(empty); + + // Add the redirected message in the references so that threading + // will work when the new recipient eventually replies to the + // original sender. + nsAutoCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + if (isFirstPass) { + nsAutoCString reference; + reference.Append('<'); + reference.Append(messageId); + reference.Append('>'); + m_compFields->SetReferences(reference.get()); + } else { + nsAutoCString references; + m_compFields->GetReferences(getter_Copies(references)); + references.AppendLiteral(" <"); + references.Append(messageId); + references.Append('>'); + m_compFields->SetReferences(references.get()); + } + break; + } + } + } + isFirstPass = false; + uri = nextUri + 1; + } while (nextUri); + PR_Free(uriList); + return rv; +} + +NS_IMETHODIMP nsMsgCompose::GetProgress(nsIMsgProgress** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + NS_IF_ADDREF(*_retval = mProgress); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetMessageSend(nsIMsgSend** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + NS_IF_ADDREF(*_retval = mMsgSend); + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::SetMessageSend(nsIMsgSend* aMsgSend) { + mMsgSend = aMsgSend; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::ClearMessageSend() { + mMsgSend = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::SetCiteReference(nsString citeReference) { + mCiteReference = citeReference; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::SetSavedFolderURI(const nsACString& folderURI) { + m_folderName = folderURI; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetSavedFolderURI(nsACString& folderURI) { + folderURI = m_folderName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetOriginalMsgURI(nsACString& originalMsgURI) { + originalMsgURI = mOriginalMsgURI; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////////// +// THIS IS THE CLASS THAT IS THE STREAM CONSUMER OF THE HTML OUTPUT +// FROM LIBMIME. THIS IS FOR QUOTING +//////////////////////////////////////////////////////////////////////////////////// +QuotingOutputStreamListener::~QuotingOutputStreamListener() {} + +QuotingOutputStreamListener::QuotingOutputStreamListener( + nsIMsgDBHdr* originalMsgHdr, bool quoteHeaders, bool headersOnly, + nsIMsgIdentity* identity, nsIMsgQuote* msgQuote, bool quoteOriginal, + const nsACString& htmlToQuote) { + nsresult rv; + mQuoteHeaders = quoteHeaders; + mHeadersOnly = headersOnly; + mIdentity = identity; + mOrigMsgHdr = originalMsgHdr; + mUnicodeBufferCharacterLength = 0; + mQuoteOriginal = quoteOriginal; + mHtmlToQuote = htmlToQuote; + mQuote = msgQuote; + + if (!mHeadersOnly || !mHtmlToQuote.IsEmpty()) { + // Get header type, locale and strings from pref. + int32_t replyHeaderType; + nsString replyHeaderAuthorWrote; + nsString replyHeaderOnDateAuthorWrote; + nsString replyHeaderAuthorWroteOnDate; + nsString replyHeaderOriginalmessage; + GetReplyHeaderInfo( + &replyHeaderType, replyHeaderAuthorWrote, replyHeaderOnDateAuthorWrote, + replyHeaderAuthorWroteOnDate, replyHeaderOriginalmessage); + + // For the built message body... + if (originalMsgHdr && !quoteHeaders) { + // Setup the cite information.... + nsCString myGetter; + if (NS_SUCCEEDED(originalMsgHdr->GetMessageId(getter_Copies(myGetter)))) { + if (!myGetter.IsEmpty()) { + nsAutoCString buf; + mCiteReference.AssignLiteral("mid:"); + MsgEscapeURL(myGetter, + nsINetUtil::ESCAPE_URL_FILE_BASENAME | + nsINetUtil::ESCAPE_URL_FORCED, + buf); + mCiteReference.Append(NS_ConvertASCIItoUTF16(buf)); + } + } + + bool citingHeader; // Do we have a header needing to cite any info from + // original message? + bool headerDate; // Do we have a header needing to cite date/time from + // original message? + switch (replyHeaderType) { + case 0: // No reply header at all (actually the "---- original message + // ----" string, which is kinda misleading. TODO: Should there + // be a "really no header" option? + mCitePrefix.Assign(replyHeaderOriginalmessage); + citingHeader = false; + headerDate = false; + break; + + case 2: // Insert both the original author and date in the reply header + // (date followed by author) + mCitePrefix.Assign(replyHeaderOnDateAuthorWrote); + citingHeader = true; + headerDate = true; + break; + + case 3: // Insert both the original author and date in the reply header + // (author followed by date) + mCitePrefix.Assign(replyHeaderAuthorWroteOnDate); + citingHeader = true; + headerDate = true; + break; + + case 4: // TODO bug 107884: implement a more featureful user specified + // header + case 1: + default: // Default is to only show the author. + mCitePrefix.Assign(replyHeaderAuthorWrote); + citingHeader = true; + headerDate = false; + break; + } + + if (citingHeader) { + int32_t placeholderIndex = kNotFound; + + if (headerDate) { + PRTime originalMsgDate; + rv = originalMsgHdr->GetDate(&originalMsgDate); + if (NS_SUCCEEDED(rv)) { + nsAutoString citeDatePart; + if ((placeholderIndex = mCitePrefix.Find(u"#2")) != kNotFound) { + mozilla::intl::DateTimeFormat::StyleBag style; + style.date = + mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + rv = mozilla::intl::AppDateTimeFormat::Format( + style, originalMsgDate, citeDatePart); + if (NS_SUCCEEDED(rv)) + mCitePrefix.Replace(placeholderIndex, 2, citeDatePart); + } + if ((placeholderIndex = mCitePrefix.Find(u"#3")) != kNotFound) { + mozilla::intl::DateTimeFormat::StyleBag style; + style.time = + mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + rv = mozilla::intl::AppDateTimeFormat::Format( + style, originalMsgDate, citeDatePart); + if (NS_SUCCEEDED(rv)) + mCitePrefix.Replace(placeholderIndex, 2, citeDatePart); + } + } + } + + if ((placeholderIndex = mCitePrefix.Find(u"#1")) != kNotFound) { + nsAutoCString author; + rv = originalMsgHdr->GetAuthor(getter_Copies(author)); + if (NS_SUCCEEDED(rv)) { + nsAutoString citeAuthor; + ExtractName(EncodedHeader(author), citeAuthor); + mCitePrefix.Replace(placeholderIndex, 2, citeAuthor); + } + } + } + } + + // This should not happen, but just in case. + if (mCitePrefix.IsEmpty()) { + mCitePrefix.AppendLiteral("\n\n"); + mCitePrefix.Append(replyHeaderOriginalmessage); + mCitePrefix.AppendLiteral("\n"); + } + } +} + +/** + * The formatflowed parameter directs if formatflowed should be used in the + * conversion. format=flowed (RFC 2646) is a way to represent flow in a plain + * text mail, without disturbing the plain text. + */ +nsresult QuotingOutputStreamListener::ConvertToPlainText(bool formatflowed, + bool formatted, + bool disallowBreaks) { + nsresult rv = + ConvertBufToPlainText(mMsgBody, formatflowed, formatted, disallowBreaks); + NS_ENSURE_SUCCESS(rv, rv); + return ConvertBufToPlainText(mSignature, formatflowed, formatted, + disallowBreaks); +} + +NS_IMETHODIMP QuotingOutputStreamListener::OnStartRequest(nsIRequest* request) { + return NS_OK; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +QuotingOutputStreamListener::OnStopRequest(nsIRequest* request, + nsresult status) { + nsresult rv = NS_OK; + + if (!mHtmlToQuote.IsEmpty()) { + // If we had a selection in the original message to quote, we can add + // it now that we are done ignoring the original body of the message + mHeadersOnly = false; + rv = AppendToMsgBody(mHtmlToQuote); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMsgCompose> compose = do_QueryReferent(mWeakComposeObj); + NS_ENSURE_TRUE(compose, NS_ERROR_NULL_POINTER); + + MSG_ComposeType type; + compose->GetType(&type); + + // Assign cite information if available... + if (!mCiteReference.IsEmpty()) compose->SetCiteReference(mCiteReference); + + bool overrideReplyTo = + mozilla::Preferences::GetBool("mail.override_list_reply_to", true); + + if (mHeaders && + (type == nsIMsgCompType::Reply || type == nsIMsgCompType::ReplyAll || + type == nsIMsgCompType::ReplyToList || + type == nsIMsgCompType::ReplyToSender || + type == nsIMsgCompType::ReplyToGroup || + type == nsIMsgCompType::ReplyToSenderAndGroup) && + mQuoteOriginal) { + nsCOMPtr<nsIMsgCompFields> compFields; + compose->GetCompFields(getter_AddRefs(compFields)); + if (compFields) { + nsAutoString from; + nsAutoString to; + nsAutoString cc; + nsAutoString bcc; + nsAutoString replyTo; + nsAutoString mailReplyTo; + nsAutoString mailFollowupTo; + nsAutoString newgroups; + nsAutoString followUpTo; + nsAutoString messageId; + nsAutoString references; + nsAutoString listPost; + + nsCString outCString; // Temp helper string. + + bool needToRemoveDup = false; + if (!mMimeConverter) { + mMimeConverter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCString charset("UTF-8"); + + mHeaders->ExtractHeader(HEADER_FROM, true, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, from); + + mHeaders->ExtractHeader(HEADER_TO, true, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, to); + + mHeaders->ExtractHeader(HEADER_CC, true, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, cc); + + mHeaders->ExtractHeader(HEADER_BCC, true, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, bcc); + + mHeaders->ExtractHeader(HEADER_MAIL_FOLLOWUP_TO, true, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, mailFollowupTo); + + mHeaders->ExtractHeader(HEADER_REPLY_TO, false, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, replyTo); + + mHeaders->ExtractHeader(HEADER_MAIL_REPLY_TO, true, outCString); + nsMsgI18NConvertRawBytesToUTF16(outCString, charset, mailReplyTo); + + mHeaders->ExtractHeader(HEADER_NEWSGROUPS, false, outCString); + if (!outCString.IsEmpty()) + mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false, + true, newgroups); + + mHeaders->ExtractHeader(HEADER_FOLLOWUP_TO, false, outCString); + if (!outCString.IsEmpty()) + mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false, + true, followUpTo); + + mHeaders->ExtractHeader(HEADER_MESSAGE_ID, false, outCString); + if (!outCString.IsEmpty()) + mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false, + true, messageId); + + mHeaders->ExtractHeader(HEADER_REFERENCES, false, outCString); + if (!outCString.IsEmpty()) + mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false, + true, references); + + mHeaders->ExtractHeader(HEADER_LIST_POST, true, outCString); + if (!outCString.IsEmpty()) + mMimeConverter->DecodeMimeHeader(outCString.get(), charset.get(), false, + true, listPost); + if (!listPost.IsEmpty()) { + int32_t startPos = listPost.Find(u"<mailto:"); + int32_t endPos = listPost.FindChar('>', startPos); + // Extract the e-mail address. + if (endPos > startPos) { + const uint32_t mailtoLen = strlen("<mailto:"); + listPost = Substring(listPost, startPos + mailtoLen, + endPos - (startPos + mailtoLen)); + } + } + + nsCString fromEmailAddress; + ExtractEmail(EncodedHeaderW(from), fromEmailAddress); + + nsTArray<nsCString> toEmailAddresses; + ExtractEmails(EncodedHeaderW(to), UTF16ArrayAdapter<>(toEmailAddresses)); + + nsTArray<nsCString> ccEmailAddresses; + ExtractEmails(EncodedHeaderW(cc), UTF16ArrayAdapter<>(ccEmailAddresses)); + + nsCOMPtr<nsIPrefBranch> prefs( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + bool replyToSelfCheckAll = false; + prefs->GetBoolPref("mailnews.reply_to_self_check_all_ident", + &replyToSelfCheckAll); + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgIdentity>> identities; + nsCString accountKey; + mOrigMsgHdr->GetAccountKey(getter_Copies(accountKey)); + if (replyToSelfCheckAll) { + // Check all available identities if the pref was set. + accountManager->GetAllIdentities(identities); + } else if (!accountKey.IsEmpty()) { + // Check headers to see which account the message came in from + // (only works for pop3). + nsCOMPtr<nsIMsgAccount> account; + accountManager->GetAccount(accountKey, getter_AddRefs(account)); + if (account) { + rv = account->GetIdentities(identities); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Check identities only for the server of the folder that the message + // is in. + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = mOrigMsgHdr->GetFolder(getter_AddRefs(msgFolder)); + + if (NS_SUCCEEDED(rv) && msgFolder) { + nsCOMPtr<nsIMsgIncomingServer> nsIMsgIncomingServer; + rv = msgFolder->GetServer(getter_AddRefs(nsIMsgIncomingServer)); + + if (NS_SUCCEEDED(rv) && nsIMsgIncomingServer) { + rv = accountManager->GetIdentitiesForServer(nsIMsgIncomingServer, + identities); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + bool isReplyToSelf = false; + nsCOMPtr<nsIMsgIdentity> selfIdentity; + if (!identities.IsEmpty()) { + nsTArray<nsCString> toEmailAddressesLower(toEmailAddresses.Length()); + for (auto email : toEmailAddresses) { + ToLowerCase(email); + toEmailAddressesLower.AppendElement(email); + } + nsTArray<nsCString> ccEmailAddressesLower(ccEmailAddresses.Length()); + for (auto email : ccEmailAddresses) { + ToLowerCase(email); + ccEmailAddressesLower.AppendElement(email); + } + + // Go through the identities to see if any of them is the author of + // the email. + for (auto lookupIdentity : identities) { + selfIdentity = lookupIdentity; + + nsCString curIdentityEmail; + lookupIdentity->GetEmail(curIdentityEmail); + + // See if it's a reply to own message, but not a reply between + // identities. + if (curIdentityEmail.Equals(fromEmailAddress, + nsCaseInsensitiveCStringComparator)) { + isReplyToSelf = true; + // For a true reply-to-self, none of your identities are normally in + // To or Cc. We need to avoid doing a reply-to-self for people that + // have multiple identities set and sometimes *uses* the other + // identity and sometimes *mails* the other identity. + // E.g. husband+wife or own-email+company-role-mail. + for (auto lookupIdentity2 : identities) { + nsCString curIdentityEmail2; + lookupIdentity2->GetEmail(curIdentityEmail2); + ToLowerCase(curIdentityEmail2); + if (toEmailAddressesLower.Contains(curIdentityEmail2)) { + // However, "From:me To:me" should be treated as + // reply-to-self if we have a Bcc. If we don't have a Bcc we + // might have the case of a generated mail of the style + // "From:me To:me Reply-To:customer". Then we need to to do a + // normal reply to the customer. + isReplyToSelf = !bcc.IsEmpty(); // true if bcc is set + break; + } else if (ccEmailAddressesLower.Contains(curIdentityEmail2)) { + // If you auto-Cc yourself your email would be in Cc - but we + // can't detect why it is in Cc so lets just treat it like a + // normal reply. + isReplyToSelf = false; + break; + } + } + break; + } + } + } + if (type == nsIMsgCompType::ReplyToSenderAndGroup || + type == nsIMsgCompType::ReplyToSender || + type == nsIMsgCompType::Reply) { + if (isReplyToSelf) { + // Cast to concrete class. We *only* what to change m_identity, not + // all the things compose->SetIdentity would do. + nsMsgCompose* _compose = static_cast<nsMsgCompose*>(compose.get()); + _compose->m_identity = selfIdentity; + compFields->SetFrom(from); + compFields->SetTo(to); + compFields->SetReplyTo(replyTo); + } else if (!mailReplyTo.IsEmpty()) { + // handle Mail-Reply-To (http://cr.yp.to/proto/replyto.html) + compFields->SetTo(mailReplyTo); + needToRemoveDup = true; + } else if (!replyTo.IsEmpty()) { + // default reply behaviour then + + if (overrideReplyTo && !listPost.IsEmpty() && + replyTo.Find(listPost) != kNotFound) { + // Reply-To munging in this list post. Reply to From instead, + // as the user can choose Reply List if that's what he wants. + compFields->SetTo(from); + } else { + compFields->SetTo(replyTo); + } + needToRemoveDup = true; + } else { + compFields->SetTo(from); + } + } else if (type == nsIMsgCompType::ReplyAll) { + if (isReplyToSelf) { + // Cast to concrete class. We *only* what to change m_identity, not + // all the things compose->SetIdentity would do. + nsMsgCompose* _compose = static_cast<nsMsgCompose*>(compose.get()); + _compose->m_identity = selfIdentity; + compFields->SetFrom(from); + compFields->SetTo(to); + compFields->SetCc(cc); + // In case it's a reply to self, but it's not the actual source of the + // sent message, then we won't know the Bcc header. So set it only if + // it's not empty. If you have auto-bcc and removed the auto-bcc for + // the original mail, you will have to do it manually for this reply + // too. + if (!bcc.IsEmpty()) compFields->SetBcc(bcc); + compFields->SetReplyTo(replyTo); + needToRemoveDup = true; + } else if (mailFollowupTo.IsEmpty()) { + // default reply-all behaviour then + + nsAutoString allTo; + if (!replyTo.IsEmpty()) { + allTo.Assign(replyTo); + needToRemoveDup = true; + if (overrideReplyTo && !listPost.IsEmpty() && + replyTo.Find(listPost) != kNotFound) { + // Reply-To munging in this list. Add From to recipients, it's the + // lesser evil... + allTo.AppendLiteral(", "); + allTo.Append(from); + } + } else { + allTo.Assign(from); + } + + allTo.AppendLiteral(", "); + allTo.Append(to); + compFields->SetTo(allTo); + + nsAutoString allCc; + compFields->GetCc(allCc); // auto-cc + if (!allCc.IsEmpty()) allCc.AppendLiteral(", "); + allCc.Append(cc); + compFields->SetCc(allCc); + + needToRemoveDup = true; + } else { + // Handle Mail-Followup-To (http://cr.yp.to/proto/replyto.html) + compFields->SetTo(mailFollowupTo); + needToRemoveDup = true; // To remove possible self from To. + + // If Cc is set a this point it's auto-Ccs, so we'll just keep those. + } + } else if (type == nsIMsgCompType::ReplyToList) { + compFields->SetTo(listPost); + } + + if (!newgroups.IsEmpty()) { + if ((type != nsIMsgCompType::Reply) && + (type != nsIMsgCompType::ReplyToSender)) + compFields->SetNewsgroups(newgroups); + if (type == nsIMsgCompType::ReplyToGroup) + compFields->SetTo(EmptyString()); + } + + if (!followUpTo.IsEmpty()) { + // Handle "followup-to: poster" magic keyword here + if (followUpTo.EqualsLiteral("poster")) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + compose->GetDomWindow(getter_AddRefs(domWindow)); + NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE); + nsMsgDisplayMessageByName(domWindow, "followupToSenderMessage"); + + if (!replyTo.IsEmpty()) { + compFields->SetTo(replyTo); + } else { + // If reply-to is empty, use the From header to fetch the original + // sender's email. + compFields->SetTo(from); + } + + // Clear the newsgroup: header field, because followup-to: poster + // only follows up to the original sender + if (!newgroups.IsEmpty()) compFields->SetNewsgroups(EmptyString()); + } else // Process "followup-to: newsgroup-content" here + { + if (type != nsIMsgCompType::ReplyToSender) + compFields->SetNewsgroups(followUpTo); + if (type == nsIMsgCompType::Reply) { + compFields->SetTo(EmptyString()); + } + } + } + + if (!references.IsEmpty()) references.Append(char16_t(' ')); + references += messageId; + compFields->SetReferences(NS_LossyConvertUTF16toASCII(references).get()); + + nsAutoCString resultStr; + + // Cast interface to concrete class that has direct field getters etc. + nsMsgCompFields* _compFields = + static_cast<nsMsgCompFields*>(compFields.get()); + + // Remove duplicate addresses between To && Cc. + if (needToRemoveDup) { + nsCString addressesToRemoveFromCc; + if (mIdentity) { + bool removeMyEmailInCc = true; + nsCString myEmail; + // Get senders address from composeField or from identity, + nsAutoCString sender(_compFields->GetFrom()); + ExtractEmail(EncodedHeader(sender), myEmail); + if (myEmail.IsEmpty()) mIdentity->GetEmail(myEmail); + + // Remove my own address from To, unless it's a reply to self. + if (!isReplyToSelf) { + RemoveDuplicateAddresses(nsDependentCString(_compFields->GetTo()), + myEmail, resultStr); + _compFields->SetTo(resultStr.get()); + } + addressesToRemoveFromCc.Assign(_compFields->GetTo()); + + // Remove own address from CC unless we want it in there + // through the automatic-CC-to-self (see bug 584962). There are + // three cases: + // - user has no automatic CC + // - user has automatic CC but own email is not in it + // - user has automatic CC and own email in it + // Only in the last case do we want our own email address to stay + // in the CC list. + bool automaticCc; + mIdentity->GetDoCc(&automaticCc); + if (automaticCc) { + nsCString autoCcList; + mIdentity->GetDoCcList(autoCcList); + nsTArray<nsCString> autoCcEmailAddresses; + ExtractEmails(EncodedHeader(autoCcList), + UTF16ArrayAdapter<>(autoCcEmailAddresses)); + if (autoCcEmailAddresses.Contains(myEmail)) { + removeMyEmailInCc = false; + } + } + + if (removeMyEmailInCc) { + addressesToRemoveFromCc.AppendLiteral(", "); + addressesToRemoveFromCc.Append(myEmail); + } + } + RemoveDuplicateAddresses(nsDependentCString(_compFields->GetCc()), + addressesToRemoveFromCc, resultStr); + _compFields->SetCc(resultStr.get()); + if (_compFields->GetBcc()) { + // Remove addresses already in Cc from Bcc. + RemoveDuplicateAddresses(nsDependentCString(_compFields->GetBcc()), + nsDependentCString(_compFields->GetCc()), + resultStr); + if (!resultStr.IsEmpty()) { + // Remove addresses already in To from Bcc. + RemoveDuplicateAddresses( + resultStr, nsDependentCString(_compFields->GetTo()), resultStr); + } + _compFields->SetBcc(resultStr.get()); + } + } + } + } + +#ifdef MSGCOMP_TRACE_PERFORMANCE + nsCOMPtr<nsIMsgComposeService> composeService( + do_GetService("@mozilla.org/messengercompose;1")); + composeService->TimeStamp( + "Done with MIME. Now we're updating the UI elements", false); +#endif + + if (mQuoteOriginal) + compose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeFieldsReady, NS_OK); + +#ifdef MSGCOMP_TRACE_PERFORMANCE + composeService->TimeStamp( + "Addressing widget, window title and focus are now set, time to insert " + "the body", + false); +#endif + + if (!mHeadersOnly) mMsgBody.AppendLiteral("</html>"); + + // Now we have an HTML representation of the quoted message. + // If we are in plain text mode, we need to convert this to plain + // text before we try to insert it into the editor. If we don't, we + // just get lots of HTML text in the message...not good. + // + // XXX not m_composeHTML? /BenB + bool composeHTML = true; + compose->GetComposeHTML(&composeHTML); + if (!composeHTML) { + // Downsampling. + + // In plain text quotes we always allow line breaking to not end up with + // long lines. The quote is inserted into a span with style + // "white-space: pre;" which isn't be wrapped. + // Update: Bug 387687 changed this to "white-space: pre-wrap;". + // Note that the body of the plain text message is wrapped since it uses + // "white-space: pre-wrap; width: 72ch;". + // Look at it in the DOM Inspector to see it. + // + // If we're using format flowed, we need to pass it so the encoder + // can add a space at the end. + nsCOMPtr<nsIPrefBranch> pPrefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + bool flowed = false; + if (pPrefBranch) { + pPrefBranch->GetBoolPref("mailnews.send_plaintext_flowed", &flowed); + } + + rv = ConvertToPlainText(flowed, + true, // formatted + false); // allow line breaks + NS_ENSURE_SUCCESS(rv, rv); + } + + compose->ProcessSignature(mIdentity, true, &mSignature); + + nsCOMPtr<nsIEditor> editor; + if (NS_SUCCEEDED(compose->GetEditor(getter_AddRefs(editor))) && editor) { + if (mQuoteOriginal) + compose->ConvertAndLoadComposeWindow(mCitePrefix, mMsgBody, mSignature, + true, composeHTML); + else + InsertToCompose(editor, composeHTML); + } + + if (mQuoteOriginal) + compose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeBodyReady, NS_OK); + return rv; +} + +NS_IMETHODIMP QuotingOutputStreamListener::OnDataAvailable( + nsIRequest* request, nsIInputStream* inStr, uint64_t sourceOffset, + uint32_t count) { + nsresult rv = NS_OK; + NS_ENSURE_ARG(inStr); + + if (mHeadersOnly) return rv; + + char* newBuf = (char*)PR_Malloc(count + 1); + if (!newBuf) return NS_ERROR_FAILURE; + + uint32_t numWritten = 0; + rv = inStr->Read(newBuf, count, &numWritten); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; + newBuf[numWritten] = '\0'; + if (NS_SUCCEEDED(rv) && numWritten > 0) { + rv = AppendToMsgBody(nsDependentCString(newBuf, numWritten)); + } + + PR_FREEIF(newBuf); + return rv; +} + +nsresult QuotingOutputStreamListener::AppendToMsgBody(const nsCString& inStr) { + nsresult rv = NS_OK; + if (!inStr.IsEmpty()) { + nsAutoString tmp; + rv = UTF_8_ENCODING->DecodeWithoutBOMHandling(inStr, tmp); + if (NS_SUCCEEDED(rv)) mMsgBody.Append(tmp); + } + return rv; +} + +nsresult QuotingOutputStreamListener::SetComposeObj(nsIMsgCompose* obj) { + mWeakComposeObj = do_GetWeakReference(obj); + return NS_OK; +} + +NS_IMETHODIMP +QuotingOutputStreamListener::SetMimeHeaders(nsIMimeHeaders* headers) { + mHeaders = headers; + return NS_OK; +} + +nsresult QuotingOutputStreamListener::InsertToCompose(nsIEditor* aEditor, + bool aHTMLEditor) { + NS_ENSURE_ARG(aEditor); + nsCOMPtr<nsINode> nodeInserted; + + TranslateLineEnding(mMsgBody); + + // Now, insert it into the editor... + aEditor->EnableUndo(true); + + nsCOMPtr<nsIMsgCompose> compose = do_QueryReferent(mWeakComposeObj); + if (!mMsgBody.IsEmpty() && compose) { + compose->SetAllowRemoteContent(true); + if (!mCitePrefix.IsEmpty()) { + if (!aHTMLEditor) mCitePrefix.AppendLiteral("\n"); + aEditor->InsertText(mCitePrefix); + } + + RefPtr<mozilla::HTMLEditor> htmlEditor = aEditor->AsHTMLEditor(); + if (aHTMLEditor) { + nsAutoString body(mMsgBody); + remove_plaintext_tag(body); + htmlEditor->InsertAsCitedQuotation(body, EmptyString(), true, + getter_AddRefs(nodeInserted)); + } else { + htmlEditor->InsertAsQuotation(mMsgBody, getter_AddRefs(nodeInserted)); + } + compose->SetAllowRemoteContent(false); + } + + RefPtr<Selection> selection; + nsCOMPtr<nsINode> parent; + int32_t offset; + nsresult rv; + + // get parent and offset of mailcite + rv = GetNodeLocation(nodeInserted, address_of(parent), &offset); + NS_ENSURE_SUCCESS(rv, rv); + + // get selection + aEditor->GetSelection(getter_AddRefs(selection)); + if (selection) { + // place selection after mailcite + selection->CollapseInLimiter(parent, offset + 1); + // insert a break at current selection + aEditor->InsertLineBreak(); + selection->CollapseInLimiter(parent, offset + 1); + } + nsCOMPtr<nsISelectionController> selCon; + aEditor->GetSelectionController(getter_AddRefs(selCon)); + + if (selCon) + // After ScrollSelectionIntoView(), the pending notifications might be + // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. + selCon->ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_ANCHOR_REGION, true); + + return NS_OK; +} + +/** + * Returns true if the domain is a match for the given the domain list. + * Subdomains are also considered to match. + * @param aDomain - the domain name to check + * @param aDomainList - a comma separated string of domain names + */ +bool IsInDomainList(const nsAString& aDomain, const nsAString& aDomainList) { + if (aDomain.IsEmpty() || aDomainList.IsEmpty()) return false; + + // Check plain text domains. + int32_t left = 0; + int32_t right = 0; + while (right != (int32_t)aDomainList.Length()) { + right = aDomainList.FindChar(',', left); + if (right == kNotFound) right = aDomainList.Length(); + nsDependentSubstring domain = Substring(aDomainList, left, right); + + if (aDomain.Equals(domain, nsCaseInsensitiveStringComparator)) return true; + + nsAutoString dotDomain; + dotDomain.Assign(u'.'); + dotDomain.Append(domain); + if (StringEndsWith(aDomain, dotDomain, nsCaseInsensitiveStringComparator)) + return true; + + left = right + 1; + } + return false; +} + +NS_IMPL_ISUPPORTS(QuotingOutputStreamListener, + nsIMsgQuotingOutputStreamListener, nsIRequestObserver, + nsIStreamListener, nsISupportsWeakReference) + +//////////////////////////////////////////////////////////////////////////////////// +// END OF QUOTING LISTENER +//////////////////////////////////////////////////////////////////////////////////// + +/* attribute MSG_ComposeType type; */ +NS_IMETHODIMP nsMsgCompose::SetType(MSG_ComposeType aType) { + mType = aType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetType(MSG_ComposeType* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = mType; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::QuoteMessage(const nsACString& msgURI) { + nsresult rv; + mQuotingToFollow = false; + + // Create a mime parser (nsIStreamConverter)! + mQuote = do_CreateInstance("@mozilla.org/messengercompose/quoting;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(msgURI, getter_AddRefs(msgHdr)); + + // Create the consumer output stream.. this will receive all the HTML from + // libmime + mQuoteStreamListener = + new QuotingOutputStreamListener(msgHdr, false, !mHtmlToQuote.IsEmpty(), + m_identity, mQuote, false, mHtmlToQuote); + + mQuoteStreamListener->SetComposeObj(this); + + rv = mQuote->QuoteMessage(msgURI, false, mQuoteStreamListener, + mAutodetectCharset, false, msgHdr); + return rv; +} + +nsresult nsMsgCompose::QuoteOriginalMessage() // New template +{ + nsresult rv; + + mQuotingToFollow = false; + + // Create a mime parser (nsIStreamConverter)! + mQuote = do_CreateInstance("@mozilla.org/messengercompose/quoting;1", &rv); + if (NS_FAILED(rv) || !mQuote) return NS_ERROR_FAILURE; + + bool bAutoQuote = true; + m_identity->GetAutoQuote(&bAutoQuote); + + nsCOMPtr<nsIMsgDBHdr> originalMsgHdr = mOrigMsgHdr; + if (!originalMsgHdr) { + rv = GetMsgDBHdrFromURI(mOriginalMsgURI, getter_AddRefs(originalMsgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoCString msgUri(mOriginalMsgURI); + bool fileUrl = StringBeginsWith(msgUri, "file:"_ns); + if (fileUrl) { + msgUri.Replace(0, 5, "mailbox:"_ns); + msgUri.AppendLiteral("?number=0"); + } + + // Create the consumer output stream.. this will receive all the HTML from + // libmime + mQuoteStreamListener = new QuotingOutputStreamListener( + originalMsgHdr, mWhatHolder != 1, !bAutoQuote || !mHtmlToQuote.IsEmpty(), + m_identity, mQuote, true, mHtmlToQuote); + + mQuoteStreamListener->SetComposeObj(this); + + rv = mQuote->QuoteMessage(msgUri, mWhatHolder != 1, mQuoteStreamListener, + mAutodetectCharset, !bAutoQuote, originalMsgHdr); + return rv; +} + +// CleanUpRecipient will remove un-necessary "<>" when a recipient as an address +// without name +void nsMsgCompose::CleanUpRecipients(nsString& recipients) { + uint16_t i; + bool startANewRecipient = true; + bool removeBracket = false; + nsAutoString newRecipient; + char16_t aChar; + + for (i = 0; i < recipients.Length(); i++) { + aChar = recipients[i]; + switch (aChar) { + case '<': + if (startANewRecipient) + removeBracket = true; + else + newRecipient += aChar; + startANewRecipient = false; + break; + + case '>': + if (removeBracket) + removeBracket = false; + else + newRecipient += aChar; + break; + + case ' ': + newRecipient += aChar; + break; + + case ',': + newRecipient += aChar; + startANewRecipient = true; + removeBracket = false; + break; + + default: + newRecipient += aChar; + startANewRecipient = false; + break; + } + } + recipients = newRecipient; +} + +NS_IMETHODIMP nsMsgCompose::RememberQueuedDisposition() { + // need to find the msg hdr in the saved folder and then set a property on + // the header that we then look at when we actually send the message. + nsresult rv; + nsAutoCString dispositionSetting; + + if (mType == nsIMsgCompType::Reply || mType == nsIMsgCompType::ReplyAll || + mType == nsIMsgCompType::ReplyToList || + mType == nsIMsgCompType::ReplyToGroup || + mType == nsIMsgCompType::ReplyToSender || + mType == nsIMsgCompType::ReplyToSenderAndGroup) { + dispositionSetting.AssignLiteral("replied"); + } else if (mType == nsIMsgCompType::ForwardAsAttachment || + mType == nsIMsgCompType::ForwardInline) { + dispositionSetting.AssignLiteral("forwarded"); + } else if (mType == nsIMsgCompType::Redirect) { + dispositionSetting.AssignLiteral("redirected"); + } else if (mType == nsIMsgCompType::Draft) { + nsAutoCString curDraftIdURL; + rv = m_compFields->GetDraftId(curDraftIdURL); + NS_ENSURE_SUCCESS(rv, rv); + if (!curDraftIdURL.IsEmpty()) { + nsCOMPtr<nsIMsgDBHdr> draftHdr; + rv = GetMsgDBHdrFromURI(curDraftIdURL, getter_AddRefs(draftHdr)); + NS_ENSURE_SUCCESS(rv, rv); + draftHdr->GetStringProperty(QUEUED_DISPOSITION_PROPERTY, + dispositionSetting); + } + } + + nsMsgKey msgKey; + if (mMsgSend) { + mMsgSend->GetMessageKey(&msgKey); + nsCString identityKey; + + m_identity->GetKey(identityKey); + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(m_folderName, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = folder->GetMessageHeader(msgKey, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t pseudoHdrProp = 0; + msgHdr->GetUint32Property("pseudoHdr", &pseudoHdrProp); + if (pseudoHdrProp) { + // Use SetAttributeOnPendingHdr for IMAP pseudo headers, as those + // will get deleted (and properties set using SetStringProperty lost.) + nsCOMPtr<nsIMsgFolder> folder; + rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDatabase> msgDB; + rv = folder->GetMsgDatabase(getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString messageId; + mMsgSend->GetMessageId(messageId); + msgHdr->SetMessageId(messageId.get()); + if (!mOriginalMsgURI.IsEmpty()) { + msgDB->SetAttributeOnPendingHdr(msgHdr, ORIG_URI_PROPERTY, + mOriginalMsgURI.get()); + if (!dispositionSetting.IsEmpty()) + msgDB->SetAttributeOnPendingHdr(msgHdr, QUEUED_DISPOSITION_PROPERTY, + dispositionSetting.get()); + } + msgDB->SetAttributeOnPendingHdr(msgHdr, HEADER_X_MOZILLA_IDENTITY_KEY, + identityKey.get()); + } else if (msgHdr) { + if (!mOriginalMsgURI.IsEmpty()) { + msgHdr->SetStringProperty(ORIG_URI_PROPERTY, mOriginalMsgURI); + if (!dispositionSetting.IsEmpty()) + msgHdr->SetStringProperty(QUEUED_DISPOSITION_PROPERTY, + dispositionSetting); + } + msgHdr->SetStringProperty(HEADER_X_MOZILLA_IDENTITY_KEY, identityKey); + } + } + return NS_OK; +} + +nsresult nsMsgCompose::ProcessReplyFlags() { + nsresult rv; + // check to see if we were doing a reply or a forward, if we were, set the + // answered field flag on the message folder for this URI. + if (mType == nsIMsgCompType::Reply || mType == nsIMsgCompType::ReplyAll || + mType == nsIMsgCompType::ReplyToList || + mType == nsIMsgCompType::ReplyToGroup || + mType == nsIMsgCompType::ReplyToSender || + mType == nsIMsgCompType::ReplyToSenderAndGroup || + mType == nsIMsgCompType::ForwardAsAttachment || + mType == nsIMsgCompType::ForwardInline || + mType == nsIMsgCompType::Redirect || + mDraftDisposition != nsIMsgFolder::nsMsgDispositionState_None) { + if (!mOriginalMsgURI.IsEmpty()) { + nsCString msgUri(mOriginalMsgURI); + char* newStr = msgUri.BeginWriting(); + char* uri; + while (nullptr != (uri = NS_strtok(",", &newStr))) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = + GetMsgDBHdrFromURI(nsDependentCString(uri), getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (msgHdr) { + // get the folder for the message resource + nsCOMPtr<nsIMsgFolder> msgFolder; + msgHdr->GetFolder(getter_AddRefs(msgFolder)); + if (msgFolder) { + // If it's a draft with disposition, default to replied, otherwise, + // check if it's a forward. + nsMsgDispositionState dispositionSetting = + nsIMsgFolder::nsMsgDispositionState_Replied; + if (mDraftDisposition != nsIMsgFolder::nsMsgDispositionState_None) + dispositionSetting = mDraftDisposition; + else if (mType == nsIMsgCompType::ForwardAsAttachment || + mType == nsIMsgCompType::ForwardInline) + dispositionSetting = + nsIMsgFolder::nsMsgDispositionState_Forwarded; + else if (mType == nsIMsgCompType::Redirect) + dispositionSetting = + nsIMsgFolder::nsMsgDispositionState_Redirected; + + msgFolder->AddMessageDispositionState(msgHdr, dispositionSetting); + if (mType != nsIMsgCompType::ForwardAsAttachment) + break; // just safeguard + } + } + } + } + } + + return NS_OK; +} +NS_IMETHODIMP nsMsgCompose::OnStartSending(const char* aMsgID, + uint32_t aMsgSize) { + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnStartSending(aMsgID, aMsgSize); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::OnProgress(const char* aMsgID, uint32_t aProgress, + uint32_t aProgressMax) { + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnProgress(aMsgID, aProgress, aProgressMax); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::OnStatus(const char* aMsgID, const char16_t* aMsg) { + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnStatus(aMsgID, aMsg); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::OnStopSending(const char* aMsgID, nsresult aStatus, + const char16_t* aMsg, + nsIFile* returnFile) { + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnStopSending(aMsgID, aStatus, aMsg, returnFile); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::OnTransportSecurityError(const char* msgID, nsresult status, + nsITransportSecurityInfo* secInfo, + nsACString const& location) { + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnTransportSecurityError(msgID, status, secInfo, + location); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::OnSendNotPerformed(const char* aMsgID, + nsresult aStatus) { + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnSendNotPerformed(aMsgID, aStatus); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::OnGetDraftFolderURI(const char* aMsgID, + const nsACString& aFolderURI) { + m_folderName = aFolderURI; + nsTObserverArray<nsCOMPtr<nsIMsgSendListener>>::ForwardIterator iter( + mExternalSendListeners); + nsCOMPtr<nsIMsgSendListener> externalSendListener; + + while (iter.HasMore()) { + externalSendListener = iter.GetNext(); + externalSendListener->OnGetDraftFolderURI(aMsgID, aFolderURI); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////////// +// This is the listener class for both the send operation and the copy +// operation. We have to create this class to listen for message send completion +// and deal with failures in both send and copy operations +//////////////////////////////////////////////////////////////////////////////////// +NS_IMPL_ADDREF(nsMsgComposeSendListener) +NS_IMPL_RELEASE(nsMsgComposeSendListener) + +/* +NS_IMPL_QUERY_INTERFACE(nsMsgComposeSendListener, + nsIMsgComposeSendListener, + nsIMsgSendListener, + nsIMsgCopyServiceListener, + nsIWebProgressListener) +*/ +NS_INTERFACE_MAP_BEGIN(nsMsgComposeSendListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgComposeSendListener) + NS_INTERFACE_MAP_ENTRY(nsIMsgComposeSendListener) + NS_INTERFACE_MAP_ENTRY(nsIMsgSendListener) + NS_INTERFACE_MAP_ENTRY(nsIMsgCopyServiceListener) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) +NS_INTERFACE_MAP_END + +nsMsgComposeSendListener::nsMsgComposeSendListener(void) { mDeliverMode = 0; } + +nsMsgComposeSendListener::~nsMsgComposeSendListener(void) {} + +NS_IMETHODIMP nsMsgComposeSendListener::SetMsgCompose(nsIMsgCompose* obj) { + mWeakComposeObj = do_GetWeakReference(obj); + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeSendListener::SetDeliverMode( + MSG_DeliverMode deliverMode) { + mDeliverMode = deliverMode; + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnStartSending(const char* aMsgID, + uint32_t aMsgSize) { + nsresult rv; + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnStartSending(aMsgID, aMsgSize); + + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnProgress(const char* aMsgID, + uint32_t aProgress, + uint32_t aProgressMax) { + nsresult rv; + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnProgress(aMsgID, aProgress, aProgressMax); + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnStatus(const char* aMsgID, + const char16_t* aMsg) { + nsresult rv; + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnStatus(aMsgID, aMsg); + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnSendNotPerformed(const char* aMsgID, + nsresult aStatus) { + // since OnSendNotPerformed is called in the case where the user aborts the + // operation by closing the compose window, we need not do the stuff required + // for closing the windows. However we would need to do the other operations + // as below. + + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj, &rv); + if (msgCompose) + msgCompose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeProcessDone, aStatus); + + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnSendNotPerformed(aMsgID, aStatus); + + return rv; +} + +NS_IMETHODIMP +nsMsgComposeSendListener::OnTransportSecurityError( + const char* msgID, nsresult status, nsITransportSecurityInfo* secInfo, + nsACString const& location) { + nsresult rv; + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnTransportSecurityError(msgID, status, secInfo, + location); + + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnStopSending(const char* aMsgID, + nsresult aStatus, + const char16_t* aMsg, + nsIFile* returnFile) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj, &rv); + if (msgCompose) { + nsCOMPtr<nsIMsgProgress> progress; + msgCompose->GetProgress(getter_AddRefs(progress)); + + if (NS_SUCCEEDED(aStatus)) { + nsCOMPtr<nsIMsgCompFields> compFields; + msgCompose->GetCompFields(getter_AddRefs(compFields)); + + // only process the reply flags if we successfully sent the message + msgCompose->ProcessReplyFlags(); + + // See if there is a composer window + bool hasDomWindow = true; + nsCOMPtr<mozIDOMWindowProxy> domWindow; + rv = msgCompose->GetDomWindow(getter_AddRefs(domWindow)); + if (NS_FAILED(rv) || !domWindow) hasDomWindow = false; + + // Close the window ONLY if we are not going to do a save operation + nsAutoString fieldsFCC; + if (NS_SUCCEEDED(compFields->GetFcc(fieldsFCC))) { + if (!fieldsFCC.IsEmpty()) { + if (fieldsFCC.LowerCaseEqualsLiteral("nocopy://")) { + msgCompose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeProcessDone, NS_OK); + if (progress) { + progress->UnregisterListener(this); + progress->CloseProgressDialog(false); + } + if (hasDomWindow) msgCompose->CloseWindow(); + } + } + } else { + msgCompose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeProcessDone, NS_OK); + if (progress) { + progress->UnregisterListener(this); + progress->CloseProgressDialog(false); + } + if (hasDomWindow) + msgCompose->CloseWindow(); // if we fail on the simple GetFcc call, + // close the window to be safe and avoid + // windows hanging around to prevent the + // app from exiting. + } + + // Remove the current draft msg when sending draft is done. + bool deleteDraft; + msgCompose->GetDeleteDraft(&deleteDraft); + if (deleteDraft) RemoveCurrentDraftMessage(msgCompose, false, false); + } else { + msgCompose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeProcessDone, aStatus); + if (progress) { + progress->CloseProgressDialog(true); + progress->UnregisterListener(this); + } + } + } + + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnStopSending(aMsgID, aStatus, aMsg, returnFile); + + return rv; +} + +nsresult nsMsgComposeSendListener::OnGetDraftFolderURI( + const char* aMsgID, const nsACString& aFolderURI) { + nsresult rv; + nsCOMPtr<nsIMsgSendListener> composeSendListener = + do_QueryReferent(mWeakComposeObj, &rv); + if (NS_SUCCEEDED(rv) && composeSendListener) + composeSendListener->OnGetDraftFolderURI(aMsgID, aFolderURI); + + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnStartCopy() { return NS_OK; } + +nsresult nsMsgComposeSendListener::OnProgress(uint32_t aProgress, + uint32_t aProgressMax) { + return NS_OK; +} + +nsresult nsMsgComposeSendListener::OnStopCopy(nsresult aStatus) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj, &rv); + if (msgCompose) { + if (mDeliverMode == nsIMsgSend::nsMsgQueueForLater || + mDeliverMode == nsIMsgSend::nsMsgDeliverBackground || + mDeliverMode == nsIMsgSend::nsMsgSaveAsDraft) { + msgCompose->RememberQueuedDisposition(); + } + + // Ok, if we are here, we are done with the send/copy operation so + // we have to do something with the window....SHOW if failed, Close + // if succeeded + + nsCOMPtr<nsIMsgProgress> progress; + msgCompose->GetProgress(getter_AddRefs(progress)); + if (progress) { + // Unregister ourself from msg compose progress + progress->UnregisterListener(this); + progress->CloseProgressDialog(NS_FAILED(aStatus)); + } + + msgCompose->NotifyStateListeners( + nsIMsgComposeNotificationType::ComposeProcessDone, aStatus); + + if (NS_SUCCEEDED(aStatus)) { + // We should only close the window if we are done. Things like templates + // and drafts aren't done so their windows should stay open + if (mDeliverMode == nsIMsgSend::nsMsgSaveAsDraft || + mDeliverMode == nsIMsgSend::nsMsgSaveAsTemplate) { + msgCompose->NotifyStateListeners( + nsIMsgComposeNotificationType::SaveInFolderDone, aStatus); + // Remove the current draft msg when saving as draft/template is done. + msgCompose->SetDeleteDraft(true); + RemoveCurrentDraftMessage( + msgCompose, true, mDeliverMode == nsIMsgSend::nsMsgSaveAsTemplate); + } else { + // Remove (possible) draft if we're in send later mode + if (mDeliverMode == nsIMsgSend::nsMsgQueueForLater || + mDeliverMode == nsIMsgSend::nsMsgDeliverBackground) { + msgCompose->SetDeleteDraft(true); + RemoveCurrentDraftMessage(msgCompose, true, false); + } + msgCompose->CloseWindow(); + } + } + msgCompose->ClearMessageSend(); + } + + return rv; +} + +nsresult nsMsgComposeSendListener::GetMsgFolder(nsIMsgCompose* compObj, + nsIMsgFolder** msgFolder) { + nsCString folderUri; + + nsresult rv = compObj->GetSavedFolderURI(folderUri); + NS_ENSURE_SUCCESS(rv, rv); + + return GetOrCreateFolder(folderUri, msgFolder); +} + +nsresult nsMsgComposeSendListener::RemoveDraftOrTemplate(nsIMsgCompose* compObj, + nsCString msgURI, + bool isSaveTemplate) { + nsresult rv; + nsCOMPtr<nsIMsgFolder> msgFolder; + nsCOMPtr<nsIMsgDBHdr> msgDBHdr; + rv = GetMsgDBHdrFromURI(msgURI, getter_AddRefs(msgDBHdr)); + NS_ASSERTION( + NS_SUCCEEDED(rv), + "RemoveDraftOrTemplate can't get msg header DB interface pointer"); + if (NS_SUCCEEDED(rv) && msgDBHdr) { + do { // Break on failure or removal not needed. + // Get the folder for the message resource. + rv = msgDBHdr->GetFolder(getter_AddRefs(msgFolder)); + NS_ASSERTION( + NS_SUCCEEDED(rv), + "RemoveDraftOrTemplate can't get msg folder interface pointer"); + if (NS_FAILED(rv) || !msgFolder) break; + + // Only do this if it's a drafts or templates folder. + uint32_t flags; + msgFolder->GetFlags(&flags); + if (!(flags & (nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates))) + break; + // Only delete a template when saving a new one, never delete a template + // when sending. + if (!isSaveTemplate && (flags & nsMsgFolderFlags::Templates)) break; + + // Only remove if the message is actually in the db. It might have only + // been in the use cache. + nsMsgKey key; + rv = msgDBHdr->GetMessageKey(&key); + if (NS_FAILED(rv)) break; + nsCOMPtr<nsIMsgDatabase> db; + msgFolder->GetMsgDatabase(getter_AddRefs(db)); + if (!db) break; + bool containsKey = false; + db->ContainsKey(key, &containsKey); + if (!containsKey) break; + + // Ready to delete the msg. + rv = msgFolder->DeleteMessages({&*msgDBHdr}, nullptr, true, false, + nullptr, false /*allowUndo*/); + NS_ASSERTION(NS_SUCCEEDED(rv), + "RemoveDraftOrTemplate can't delete message"); + } while (false); + } else { + // If we get here we have the case where the draft folder is on the server + // and it's not currently open (in thread pane), so draft msgs are saved to + // the server but they're not in our local DB. In this case, + // GetMsgDBHdrFromURI() will never find the msg. If the draft folder is a + // local one then we'll not get here because the draft msgs are saved to the + // local folder and are in local DB. Make sure the msg folder is imap. Even + // if we get here due to DB errors (worst case), we should still try to + // delete msg on the server because that's where the master copy of the msgs + // are stored, if draft folder is on the server. For local case, since DB is + // bad we can't do anything with it anyway so it'll be noop in this case. + rv = GetMsgFolder(compObj, getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(msgFolder); + NS_ASSERTION(imapFolder, + "The draft folder MUST be an imap folder in order to mark " + "the msg delete!"); + if (NS_SUCCEEDED(rv) && imapFolder) { + // Only do this if it's a drafts or templates folder. + uint32_t flags; + msgFolder->GetFlags(&flags); + if (!(flags & (nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates))) + return NS_OK; + // Only delete a template when saving a new one, never delete a template + // when sending. + if (!isSaveTemplate && (flags & nsMsgFolderFlags::Templates)) + return NS_OK; + + const char* str = PL_strchr(msgURI.get(), '#'); + NS_ASSERTION(str, "Failed to get current draft id url"); + if (str) { + nsAutoCString srcStr(str + 1); + nsresult err; + nsMsgKey messageID = srcStr.ToInteger(&err); + if (messageID != nsMsgKey_None) { + rv = imapFolder->StoreImapFlags(kImapMsgDeletedFlag, true, + {messageID}, nullptr); + } + } + } + } + } + + return rv; +} + +/** + * Remove the current draft message since a new one will be saved. + * When we're coming to save a template, also delete the original template. + * This is necessary since auto-save doesn't delete the original template. + */ +nsresult nsMsgComposeSendListener::RemoveCurrentDraftMessage( + nsIMsgCompose* compObj, bool calledByCopy, bool isSaveTemplate) { + nsresult rv; + nsCOMPtr<nsIMsgCompFields> compFields = nullptr; + + rv = compObj->GetCompFields(getter_AddRefs(compFields)); + NS_ASSERTION(NS_SUCCEEDED(rv), + "RemoveCurrentDraftMessage can't get compose fields"); + if (NS_FAILED(rv) || !compFields) return rv; + + nsCString curDraftIdURL; + rv = compFields->GetDraftId(curDraftIdURL); + + // Skip if no draft id (probably a new draft msg). + if (NS_SUCCEEDED(rv) && !curDraftIdURL.IsEmpty()) { + rv = RemoveDraftOrTemplate(compObj, curDraftIdURL, isSaveTemplate); + if (NS_FAILED(rv)) NS_WARNING("Removing current draft failed"); + } else { + NS_WARNING("RemoveCurrentDraftMessage can't get draft id"); + } + + if (isSaveTemplate) { + nsCString templateIdURL; + rv = compFields->GetTemplateId(templateIdURL); + if (NS_SUCCEEDED(rv) && !templateIdURL.Equals(curDraftIdURL)) { + // Above we deleted an auto-saved draft, so here we need to delete + // the original template. + rv = RemoveDraftOrTemplate(compObj, templateIdURL, isSaveTemplate); + if (NS_FAILED(rv)) NS_WARNING("Removing original template failed"); + } + } + + // Now get the new uid so that next save will remove the right msg + // regardless whether or not the exiting msg can be deleted. + if (calledByCopy) { + nsMsgKey newUid = 0; + nsCOMPtr<nsIMsgFolder> savedToFolder; + nsCOMPtr<nsIMsgSend> msgSend; + rv = compObj->GetMessageSend(getter_AddRefs(msgSend)); + NS_ASSERTION(msgSend, "RemoveCurrentDraftMessage msgSend is null."); + if (NS_FAILED(rv) || !msgSend) return rv; + + rv = msgSend->GetMessageKey(&newUid); + NS_ENSURE_SUCCESS(rv, rv); + + // Make sure we have a folder interface pointer + rv = GetMsgFolder(compObj, getter_AddRefs(savedToFolder)); + + // Reset draft (uid) url with the new uid. + if (savedToFolder && newUid != nsMsgKey_None) { + uint32_t folderFlags; + savedToFolder->GetFlags(&folderFlags); + if (folderFlags & + (nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates)) { + nsCString newDraftIdURL; + rv = savedToFolder->GenerateMessageURI(newUid, newDraftIdURL); + NS_ENSURE_SUCCESS(rv, rv); + compFields->SetDraftId(newDraftIdURL); + if (isSaveTemplate) compFields->SetTemplateId(newDraftIdURL); + } + } + } + return rv; +} + +nsresult nsMsgComposeSendListener::SetMessageKey(nsMsgKey aMessageKey) { + return NS_OK; +} + +nsresult nsMsgComposeSendListener::GetMessageId(nsACString& messageId) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeSendListener::OnStateChange( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) { + if (aStateFlags == nsIWebProgressListener::STATE_STOP) { + nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(mWeakComposeObj); + if (msgCompose) { + nsCOMPtr<nsIMsgProgress> progress; + msgCompose->GetProgress(getter_AddRefs(progress)); + + // Time to stop any pending operation... + if (progress) { + // Unregister ourself from msg compose progress + progress->UnregisterListener(this); + + bool bCanceled = false; + progress->GetProcessCanceledByUser(&bCanceled); + if (bCanceled) { + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/messengercompose/" + "composeMsgs.properties", + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + nsString msg; + bundle->GetStringFromName("msgCancelling", msg); + progress->OnStatusChange(nullptr, nullptr, NS_OK, msg.get()); + } + } + + nsCOMPtr<nsIMsgSend> msgSend; + msgCompose->GetMessageSend(getter_AddRefs(msgSend)); + if (msgSend) msgSend->Abort(); + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeSendListener::OnProgressChange( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, + int32_t aCurSelfProgress, int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, int32_t aMaxTotalProgress) { + /* Ignore this call */ + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeSendListener::OnLocationChange( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* location, + uint32_t aFlags) { + /* Ignore this call */ + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeSendListener::OnStatusChange( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + /* Ignore this call */ + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeSendListener::OnSecurityChange( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t state) { + /* Ignore this call */ + return NS_OK; +} + +NS_IMETHODIMP +nsMsgComposeSendListener::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + /* Ignore this call */ + return NS_OK; +} + +nsresult nsMsgCompose::ConvertHTMLToText(nsIFile* aSigFile, + nsString& aSigData) { + nsAutoString origBuf; + + nsresult rv = LoadDataFromFile(aSigFile, origBuf); + NS_ENSURE_SUCCESS(rv, rv); + + ConvertBufToPlainText(origBuf, false, true, true); + aSigData = origBuf; + return NS_OK; +} + +nsresult nsMsgCompose::ConvertTextToHTML(nsIFile* aSigFile, + nsString& aSigData) { + nsresult rv; + nsAutoString origBuf; + + rv = LoadDataFromFile(aSigFile, origBuf); + if (NS_FAILED(rv)) return rv; + + // Ok, once we are here, we need to escape the data to make sure that + // we don't do HTML stuff with plain text sigs. + nsCString escapedUTF8; + nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(origBuf), escapedUTF8); + aSigData.Append(NS_ConvertUTF8toUTF16(escapedUTF8)); + + return NS_OK; +} + +nsresult nsMsgCompose::LoadDataFromFile(nsIFile* file, nsString& sigData, + bool aAllowUTF8, bool aAllowUTF16) { + bool isDirectory = false; + file->IsDirectory(&isDirectory); + if (isDirectory) { + NS_ERROR("file is a directory"); + return NS_MSG_ERROR_READING_FILE; + } + + nsAutoCString data; + nsresult rv = nsMsgCompose::SlurpFileToString(file, data); + NS_ENSURE_SUCCESS(rv, rv); + + const char* readBuf = data.get(); + int32_t readSize = data.Length(); + + nsAutoCString sigEncoding(nsMsgI18NParseMetaCharset(file)); + bool removeSigCharset = !sigEncoding.IsEmpty() && m_composeHTML; + + if (sigEncoding.IsEmpty()) { + if (aAllowUTF8 && mozilla::IsUtf8(nsDependentCString(readBuf))) { + sigEncoding.AssignLiteral("UTF-8"); + } else if (sigEncoding.IsEmpty() && aAllowUTF16 && readSize % 2 == 0 && + readSize >= 2 && + ((readBuf[0] == char(0xFE) && readBuf[1] == char(0xFF)) || + (readBuf[0] == char(0xFF) && readBuf[1] == char(0xFE)))) { + sigEncoding.AssignLiteral("UTF-16"); + } else { + // Autodetect encoding for plain text files w/o meta charset + nsAutoCString textFileCharset; + rv = MsgDetectCharsetFromFile(file, textFileCharset); + NS_ENSURE_SUCCESS(rv, rv); + sigEncoding.Assign(textFileCharset); + } + } + + if (NS_FAILED(nsMsgI18NConvertToUnicode(sigEncoding, data, sigData))) + CopyASCIItoUTF16(data, sigData); + + // remove sig meta charset to allow user charset override during composition + if (removeSigCharset) { + nsAutoCString metaCharset("charset="); + metaCharset.Append(sigEncoding); + int32_t pos = sigData.LowerCaseFindASCII(metaCharset); + if (pos != kNotFound) sigData.Cut(pos, metaCharset.Length()); + } + return NS_OK; +} + +/** + * If the data contains file URLs, convert them to data URLs instead. + * This is intended to be used in for signature files, so that we can make sure + * images loaded into the editor are available on send. + */ +nsresult nsMsgCompose::ReplaceFileURLs(nsString& aData) { + // XXX This code is rather incomplete since it looks for "file://" even + // outside tags. + + int32_t offset = 0; + while (true) { + int32_t fPos = aData.LowerCaseFindASCII("file://", offset); + if (fPos == kNotFound) { + break; // All done. + } + bool quoted = false; + char16_t q = 'x'; // initialise to anything to keep compilers happy. + if (fPos > 0) { + q = aData.CharAt(fPos - 1); + quoted = (q == '"' || q == '\''); + } + int32_t end = kNotFound; + if (quoted) { + end = aData.FindChar(q, fPos); + } else { + int32_t spacePos = aData.FindChar(' ', fPos); + int32_t gtPos = aData.FindChar('>', fPos); + if (gtPos != kNotFound && spacePos != kNotFound) { + end = (spacePos < gtPos) ? spacePos : gtPos; + } else if (gtPos == kNotFound && spacePos != kNotFound) { + end = spacePos; + } else if (gtPos != kNotFound && spacePos == kNotFound) { + end = gtPos; + } + } + if (end == kNotFound) { + break; + } + nsString fileURL; + fileURL = Substring(aData, fPos, end - fPos); + nsString dataURL; + nsresult rv = DataURLForFileURL(fileURL, dataURL); + if (NS_SUCCEEDED(rv)) { + aData.Replace(fPos, fileURL.Length(), dataURL); + offset = fPos + dataURL.Length(); + } else { + // If this one failed, maybe because the file wasn't found, + // continue to process the next one. + offset = fPos + fileURL.Length(); + } + } + return NS_OK; +} + +nsresult nsMsgCompose::DataURLForFileURL(const nsAString& aFileURL, + nsAString& aDataURL) { + nsresult rv; + nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> fileUri; + rv = + NS_NewURI(getter_AddRefs(fileUri), NS_ConvertUTF16toUTF8(aFileURL).get()); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(fileUri, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFile> file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString type; + rv = mime->GetTypeFromFile(file, type); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString data; + rv = nsMsgCompose::SlurpFileToString(file, data); + NS_ENSURE_SUCCESS(rv, rv); + + aDataURL.AssignLiteral("data:"); + AppendUTF8toUTF16(type, aDataURL); + + nsAutoString filename; + rv = file->GetLeafName(filename); + if (NS_SUCCEEDED(rv)) { + nsAutoCString fn; + MsgEscapeURL( + NS_ConvertUTF16toUTF8(filename), + nsINetUtil::ESCAPE_URL_FILE_BASENAME | nsINetUtil::ESCAPE_URL_FORCED, + fn); + if (!fn.IsEmpty()) { + aDataURL.AppendLiteral(";filename="); + aDataURL.Append(NS_ConvertUTF8toUTF16(fn)); + } + } + + aDataURL.AppendLiteral(";base64,"); + char* result = PL_Base64Encode(data.get(), data.Length(), nullptr); + nsDependentCString base64data(result); + NS_ENSURE_SUCCESS(rv, rv); + AppendUTF8toUTF16(base64data, aDataURL); + return NS_OK; +} + +nsresult nsMsgCompose::SlurpFileToString(nsIFile* aFile, nsACString& aString) { + aString.Truncate(); + + nsCOMPtr<nsIURI> fileURI; + nsresult rv = NS_NewFileURI(getter_AddRefs(fileURI), aFile); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), fileURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIInputStream> stream; + rv = channel->Open(getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = NS_ConsumeStream(stream, UINT32_MAX, aString); + if (NS_FAILED(rv)) { + return rv; + } + + rv = stream->Close(); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +nsresult nsMsgCompose::BuildQuotedMessageAndSignature(void) { + // + // This should never happen...if it does, just bail out... + // + NS_ASSERTION(m_editor, "BuildQuotedMessageAndSignature but no editor!"); + if (!m_editor) return NS_ERROR_FAILURE; + + // We will fire off the quote operation and wait for it to + // finish before we actually do anything with Ender... + return QuoteOriginalMessage(); +} + +// +// This will process the signature file for the user. This method +// will always append the results to the mMsgBody member variable. +// +nsresult nsMsgCompose::ProcessSignature(nsIMsgIdentity* identity, bool aQuoted, + nsString* aMsgBody) { + nsresult rv = NS_OK; + + // Now, we can get sort of fancy. This is the time we need to check + // for all sorts of user defined stuff, like signatures and editor + // types and the like! + // + // user_pref(".....sig_file", "y:\\sig.html"); + // user_pref(".....attach_signature", true); + // user_pref(".....htmlSigText", "unicode sig"); + // + // Note: We will have intelligent signature behavior in that we + // look at the signature file first...if the extension is .htm or + // .html, we assume its HTML, otherwise, we assume it is plain text + // + // ...and that's not all! What we will also do now is look and see if + // the file is an image file. If it is an image file, then we should + // insert the correct HTML into the composer to have it work, but if we + // are doing plain text compose, we should insert some sort of message + // saying "Image Signature Omitted" or something (not done yet). + // + // If there's a sig pref, it will only be used if there is no sig file + // defined, thus if attach_signature is checked, htmlSigText is ignored (bug + // 324495). Plain-text signatures may or may not have a trailing line break + // (bug 428040). + + bool attachFile = false; + bool useSigFile = false; + bool htmlSig = false; + bool imageSig = false; + nsAutoString sigData; + nsAutoString sigOutput; + int32_t reply_on_top = 0; + bool sig_bottom = true; + bool suppressSigSep = false; + + nsCOMPtr<nsIFile> sigFile; + if (identity) { + if (!CheckIncludeSignaturePrefs(identity)) return NS_OK; + + identity->GetReplyOnTop(&reply_on_top); + identity->GetSigBottom(&sig_bottom); + identity->GetSuppressSigSep(&suppressSigSep); + + rv = identity->GetAttachSignature(&attachFile); + if (NS_SUCCEEDED(rv) && attachFile) { + rv = identity->GetSignature(getter_AddRefs(sigFile)); + if (NS_SUCCEEDED(rv) && sigFile) { + if (!sigFile->NativePath().IsEmpty()) { + bool exists = false; + sigFile->Exists(&exists); + if (exists) { + useSigFile = true; // ok, there's a signature file + + // Now, most importantly, we need to figure out what the content + // type is for this signature...if we can't, we assume text + nsAutoCString sigContentType; + nsresult rv2; // don't want to clobber the other rv + nsCOMPtr<nsIMIMEService> mimeFinder( + do_GetService(NS_MIMESERVICE_CONTRACTID, &rv2)); + if (NS_SUCCEEDED(rv2)) { + rv2 = mimeFinder->GetTypeFromFile(sigFile, sigContentType); + if (NS_SUCCEEDED(rv2)) { + if (StringBeginsWith(sigContentType, "image/"_ns, + nsCaseInsensitiveCStringComparator)) + imageSig = true; + else if (sigContentType.Equals( + TEXT_HTML, nsCaseInsensitiveCStringComparator)) + htmlSig = true; + } + } + } + } + } + } + } + + // Unless signature to be attached from file, use preference value; + // the htmlSigText value is always going to be treated as html if + // the htmlSigFormat pref is true, otherwise it is considered text + nsAutoString prefSigText; + if (identity && !attachFile) identity->GetHtmlSigText(prefSigText); + // Now, if they didn't even want to use a signature, we should + // just return nicely. + // + if ((!useSigFile && prefSigText.IsEmpty()) || NS_FAILED(rv)) return NS_OK; + + static const char htmlBreak[] = "<br>"; + static const char dashes[] = "-- "; + static const char htmlsigopen[] = "<div class=\"moz-signature\">"; + static const char htmlsigclose[] = "</div>"; /* XXX: Due to a bug in + 4.x' HTML editor, it will not be able to + break this HTML sig, if quoted (for the user to + interleave a comment). */ + static const char _preopen[] = "<pre class=\"moz-signature\" cols=%d>"; + char* preopen; + static const char preclose[] = "</pre>"; + + int32_t wrapLength = 72; // setup default value in case GetWrapLength failed + GetWrapLength(&wrapLength); + preopen = PR_smprintf(_preopen, wrapLength); + if (!preopen) return NS_ERROR_OUT_OF_MEMORY; + + bool paragraphMode = + mozilla::Preferences::GetBool("mail.compose.default_to_paragraph", false); + + if (imageSig) { + // We have an image signature. If we're using the in HTML composer, we + // should put in the appropriate HTML for inclusion, otherwise, do nothing. + if (m_composeHTML) { + if (!paragraphMode) sigOutput.AppendLiteral(htmlBreak); + sigOutput.AppendLiteral(htmlsigopen); + if ((mType == nsIMsgCompType::NewsPost || !suppressSigSep) && + (reply_on_top != 1 || sig_bottom || !aQuoted)) { + sigOutput.AppendLiteral(dashes); + } + + sigOutput.AppendLiteral(htmlBreak); + sigOutput.AppendLiteral("<img src='"); + + nsCOMPtr<nsIURI> fileURI; + nsresult rv = NS_NewFileURI(getter_AddRefs(fileURI), sigFile); + NS_ENSURE_SUCCESS(rv, rv); + nsCString fileURL; + fileURI->GetSpec(fileURL); + + nsString dataURL; + rv = DataURLForFileURL(NS_ConvertUTF8toUTF16(fileURL), dataURL); + if (NS_SUCCEEDED(rv)) { + sigOutput.Append(dataURL); + } + sigOutput.AppendLiteral("' border=0>"); + sigOutput.AppendLiteral(htmlsigclose); + } + } else if (useSigFile) { + // is this a text sig with an HTML editor? + if ((m_composeHTML) && (!htmlSig)) { + ConvertTextToHTML(sigFile, sigData); + } + // is this a HTML sig with a text window? + else if ((!m_composeHTML) && (htmlSig)) { + ConvertHTMLToText(sigFile, sigData); + } else { // We have a match... + LoadDataFromFile(sigFile, sigData); // Get the data! + ReplaceFileURLs(sigData); + } + } + + // if we have a prefSigText, append it to sigData. + if (!prefSigText.IsEmpty()) { + // set htmlSig if the pref is supposed to contain HTML code, defaults to + // false + rv = identity->GetHtmlSigFormat(&htmlSig); + if (NS_FAILED(rv)) htmlSig = false; + + if (!m_composeHTML) { + if (htmlSig) ConvertBufToPlainText(prefSigText, false, true, true); + sigData.Append(prefSigText); + } else { + if (!htmlSig) { + nsCString escapedUTF8; + nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(prefSigText), escapedUTF8); + sigData.Append(NS_ConvertUTF8toUTF16(escapedUTF8)); + } else { + ReplaceFileURLs(prefSigText); + sigData.Append(prefSigText); + } + } + } + + // post-processing for plain-text signatures to ensure we end in CR, LF, or + // CRLF + if (!htmlSig && !m_composeHTML) { + int32_t sigLength = sigData.Length(); + if (sigLength > 0 && !(sigData.CharAt(sigLength - 1) == '\r') && + !(sigData.CharAt(sigLength - 1) == '\n')) + sigData.AppendLiteral(CRLF); + } + + // Now that sigData holds data...if any, append it to the body in a nice + // looking manner + if (!sigData.IsEmpty()) { + if (m_composeHTML) { + if (!paragraphMode) sigOutput.AppendLiteral(htmlBreak); + + if (htmlSig) + sigOutput.AppendLiteral(htmlsigopen); + else + sigOutput.Append(NS_ConvertASCIItoUTF16(preopen)); + } + + if ((reply_on_top != 1 || sig_bottom || !aQuoted) && + sigData.Find(u"\r-- \r") < 0 && sigData.Find(u"\n-- \n") < 0 && + sigData.Find(u"\n-- \r") < 0) { + nsDependentSubstring firstFourChars(sigData, 0, 4); + + if ((mType == nsIMsgCompType::NewsPost || !suppressSigSep) && + !(firstFourChars.EqualsLiteral("-- \n") || + firstFourChars.EqualsLiteral("-- \r"))) { + sigOutput.AppendLiteral(dashes); + + if (!m_composeHTML || !htmlSig) + sigOutput.AppendLiteral(CRLF); + else if (m_composeHTML) + sigOutput.AppendLiteral(htmlBreak); + } + } + + // add CRLF before signature for plain-text mode if signature comes before + // quote + if (!m_composeHTML && reply_on_top == 1 && !sig_bottom && aQuoted) + sigOutput.AppendLiteral(CRLF); + + sigOutput.Append(sigData); + + if (m_composeHTML) { + if (htmlSig) + sigOutput.AppendLiteral(htmlsigclose); + else + sigOutput.AppendLiteral(preclose); + } + } + + aMsgBody->Append(sigOutput); + PR_Free(preopen); + return NS_OK; +} + +nsresult nsMsgCompose::BuildBodyMessageAndSignature() { + nsresult rv = NS_OK; + + // + // This should never happen...if it does, just bail out... + // + if (!m_editor) return NS_ERROR_FAILURE; + + // + // Now, we have the body so we can just blast it into the + // composition editor window. + // + nsAutoString body; + m_compFields->GetBody(body); + + // Some time we want to add a signature and sometime we won't. + // Let's figure that out now... + bool addSignature; + bool isQuoted = false; + switch (mType) { + case nsIMsgCompType::ForwardInline: + addSignature = true; + isQuoted = true; + break; + case nsIMsgCompType::New: + case nsIMsgCompType::MailToUrl: /* same as New */ + case nsIMsgCompType::Reply: /* should not happen! but just in case */ + case nsIMsgCompType::ReplyAll: /* should not happen! but just in case */ + case nsIMsgCompType::ReplyToList: /* should not happen! but just in case */ + case nsIMsgCompType::ForwardAsAttachment: /* should not happen! but just in + case */ + case nsIMsgCompType::NewsPost: + case nsIMsgCompType::ReplyToGroup: + case nsIMsgCompType::ReplyToSender: + case nsIMsgCompType::ReplyToSenderAndGroup: + addSignature = true; + break; + + case nsIMsgCompType::Draft: + case nsIMsgCompType::Template: + case nsIMsgCompType::Redirect: + case nsIMsgCompType::EditAsNew: + addSignature = false; + break; + + default: + addSignature = false; + break; + } + + nsAutoString tSignature; + if (addSignature) ProcessSignature(m_identity, isQuoted, &tSignature); + + // if type is new, but we have body, this is probably a mapi send, so we need + // to replace '\n' with <br> so that the line breaks won't be lost by html. if + // mailtourl, do the same. + if (m_composeHTML && + (mType == nsIMsgCompType::New || mType == nsIMsgCompType::MailToUrl)) + body.ReplaceSubstring(u"\n"_ns, u"<br>"_ns); + + // Restore flowed text wrapping for Drafts/Templates. + // Look for unquoted lines - if we have an unquoted line + // that ends in a space, join this line with the next one + // by removing the end of line char(s). + int32_t wrapping_enabled = 0; + GetWrapLength(&wrapping_enabled); + if (!m_composeHTML && wrapping_enabled) { + bool quote = false; + for (uint32_t i = 0; i < body.Length(); i++) { + if (i == 0 || body[i - 1] == '\n') // newline + { + if (body[i] == '>') { + quote = true; + continue; + } + nsString s(Substring(body, i, 10)); + if (StringBeginsWith(s, u"-- \r"_ns) || + StringBeginsWith(s, u"-- \n"_ns)) { + i += 4; + continue; + } + if (StringBeginsWith(s, u"- -- \r"_ns) || + StringBeginsWith(s, u"- -- \n"_ns)) { + i += 6; + continue; + } + } + if (body[i] == '\n' && i > 1) { + if (quote) { + quote = false; + continue; // skip quoted lines + } + uint32_t j = i - 1; // look backward for space + if (body[j] == '\r') j--; + if (body[j] == ' ') // join this line with next one + body.Cut(j + 1, i - j); // remove CRLF + } + } + } + + nsString empty; + rv = ConvertAndLoadComposeWindow(empty, body, tSignature, false, + m_composeHTML); + + return rv; +} + +nsresult nsMsgCompose::NotifyStateListeners(int32_t aNotificationType, + nsresult aResult) { + nsTObserverArray<nsCOMPtr<nsIMsgComposeStateListener>>::ForwardIterator iter( + mStateListeners); + nsCOMPtr<nsIMsgComposeStateListener> thisListener; + + while (iter.HasMore()) { + thisListener = iter.GetNext(); + + switch (aNotificationType) { + case nsIMsgComposeNotificationType::ComposeFieldsReady: + thisListener->NotifyComposeFieldsReady(); + break; + + case nsIMsgComposeNotificationType::ComposeProcessDone: + thisListener->ComposeProcessDone(aResult); + break; + + case nsIMsgComposeNotificationType::SaveInFolderDone: + thisListener->SaveInFolderDone(m_folderName.get()); + break; + + case nsIMsgComposeNotificationType::ComposeBodyReady: + thisListener->NotifyComposeBodyReady(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown notification"); + break; + } + } + + return NS_OK; +} + +nsresult nsMsgCompose::AttachmentPrettyName(const nsACString& scheme, + const char* charset, + nsACString& _retval) { + nsresult rv; + + if (StringHead(scheme, 5).LowerCaseEqualsLiteral("file:")) { + nsCOMPtr<nsIFile> file; + rv = NS_GetFileFromURLSpec(scheme, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString leafName; + rv = file->GetLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF16toUTF8(leafName, _retval); + return rv; + } + + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString retUrl; + rv = textToSubURI->UnEscapeURIForUI(scheme, retUrl); + + if (NS_SUCCEEDED(rv)) { + CopyUTF16toUTF8(retUrl, _retval); + } else { + _retval.Assign(scheme); + } + if (StringHead(scheme, 5).LowerCaseEqualsLiteral("http:")) _retval.Cut(0, 7); + + return NS_OK; +} + +/** + * Retrieve address book directories and mailing lists. + * + * @param aDirUri directory URI + * @param allDirectoriesArray retrieved directories and sub-directories + * @param allMailListArray retrieved maillists + */ +nsresult nsMsgCompose::GetABDirAndMailLists( + const nsACString& aDirUri, nsCOMArray<nsIAbDirectory>& aDirArray, + nsTArray<nsMsgMailList>& aMailListArray) { + static bool collectedAddressbookFound = false; + + nsresult rv; + nsCOMPtr<nsIAbManager> abManager = + do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (aDirUri.Equals(kAllDirectoryRoot)) { + nsTArray<RefPtr<nsIAbDirectory>> directories; + rv = abManager->GetDirectories(directories); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t count = directories.Length(); + nsCString uri; + for (uint32_t i = 0; i < count; i++) { + rv = directories[i]->GetURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t pos; + if (uri.EqualsLiteral(kPersonalAddressbookUri)) { + pos = 0; + } else { + uint32_t count = aDirArray.Count(); + + if (uri.EqualsLiteral(kCollectedAddressbookUri)) { + collectedAddressbookFound = true; + pos = count; + } else { + if (collectedAddressbookFound && count > 1) { + pos = count - 1; + } else { + pos = count; + } + } + } + + aDirArray.InsertObjectAt(directories[i], pos); + rv = GetABDirAndMailLists(uri, aDirArray, aMailListArray); + } + + return NS_OK; + } + + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(aDirUri, getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIAbDirectory>> subDirectories; + rv = directory->GetChildNodes(subDirectories); + NS_ENSURE_SUCCESS(rv, rv); + for (nsIAbDirectory* subDirectory : subDirectories) { + bool bIsMailList; + if (NS_SUCCEEDED(subDirectory->GetIsMailList(&bIsMailList)) && + bIsMailList) { + aMailListArray.AppendElement(subDirectory); + } + } + return rv; +} + +/** + * Comparator for use with nsTArray::IndexOf to find a recipient. + * This comparator will check if an "address" is a mail list or not. + */ +struct nsMsgMailListComparator { + // A mail list will have one of the formats + // 1) "mName <mDescription>" when the list has a description + // 2) "mName <mName>" when the list lacks description + // A recipient is of the form "mName <mEmail>" - for equality the list + // name must be the same. The recipient "email" must match the list name for + // case 1, and the list description for case 2. + bool Equals(const nsMsgMailList& mailList, + const nsMsgRecipient& recipient) const { + if (!mailList.mName.Equals(recipient.mName, + nsCaseInsensitiveStringComparator)) + return false; + return mailList.mDescription.IsEmpty() + ? mailList.mName.Equals(recipient.mEmail, + nsCaseInsensitiveStringComparator) + : mailList.mDescription.Equals( + recipient.mEmail, nsCaseInsensitiveStringComparator); + } +}; + +/** + * Comparator for use with nsTArray::IndexOf to find a recipient. + */ +struct nsMsgRecipientComparator { + bool Equals(const nsMsgRecipient& recipient, + const nsMsgRecipient& recipientToFind) const { + if (!recipient.mEmail.Equals(recipientToFind.mEmail, + nsCaseInsensitiveStringComparator)) + return false; + + if (!recipient.mName.Equals(recipientToFind.mName, + nsCaseInsensitiveStringComparator)) + return false; + + return true; + } +}; + +/** + * This function recursively resolves a mailing list and returns individual + * email addresses. Nested lists are supported. It maintains an array of + * already visited mailing lists to avoid endless recursion. + * + * @param aMailList the list + * @param allDirectoriesArray all directories + * @param allMailListArray all maillists + * @param mailListProcessed maillists processed (to avoid recursive lists) + * @param aListMembers list members + */ +nsresult nsMsgCompose::ResolveMailList( + nsIAbDirectory* aMailList, nsCOMArray<nsIAbDirectory>& allDirectoriesArray, + nsTArray<nsMsgMailList>& allMailListArray, + nsTArray<nsMsgMailList>& mailListProcessed, + nsTArray<nsMsgRecipient>& aListMembers) { + nsresult rv = NS_OK; + + nsTArray<RefPtr<nsIAbCard>> mailListAddresses; + rv = aMailList->GetChildCards(mailListAddresses); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIAbCard* existingCard : mailListAddresses) { + nsMsgRecipient newRecipient; + + rv = existingCard->GetDisplayName(newRecipient.mName); + NS_ENSURE_SUCCESS(rv, rv); + rv = existingCard->GetPrimaryEmail(newRecipient.mEmail); + NS_ENSURE_SUCCESS(rv, rv); + + if (newRecipient.mName.IsEmpty() && newRecipient.mEmail.IsEmpty()) { + continue; + } + + // First check if it's a mailing list. + size_t index = + allMailListArray.IndexOf(newRecipient, 0, nsMsgMailListComparator()); + if (index != allMailListArray.NoIndex && + allMailListArray[index].mDirectory) { + // Check if maillist processed. + if (mailListProcessed.Contains(newRecipient, nsMsgMailListComparator())) { + continue; + } + + nsCOMPtr<nsIAbDirectory> directory2(allMailListArray[index].mDirectory); + + // Add mailList to mailListProcessed. + mailListProcessed.AppendElement(directory2); + + // Resolve mailList members. + rv = ResolveMailList(directory2, allDirectoriesArray, allMailListArray, + mailListProcessed, aListMembers); + NS_ENSURE_SUCCESS(rv, rv); + + continue; + } + + // Check if recipient is in aListMembers. + if (aListMembers.Contains(newRecipient, nsMsgRecipientComparator())) { + continue; + } + + // Now we need to insert the new address into the list of recipients. + newRecipient.mCard = existingCard; + newRecipient.mDirectory = aMailList; + + aListMembers.AppendElement(newRecipient); + } + + return rv; +} + +/** + * Lookup the recipients as specified in the compose fields (To, Cc, Bcc) + * in the address books and return an array of individual recipients. + * Mailing lists are replaced by the cards they contain, nested and recursive + * lists are taken care of, recipients contained in multiple lists are only + * added once. + * + * @param recipientsList (out) recipient array + */ +nsresult nsMsgCompose::LookupAddressBook(RecipientsArray& recipientsList) { + nsresult rv = NS_OK; + + // First, build some arrays with the original recipients. + + nsAutoString originalRecipients[MAX_OF_RECIPIENT_ARRAY]; + m_compFields->GetTo(originalRecipients[0]); + m_compFields->GetCc(originalRecipients[1]); + m_compFields->GetBcc(originalRecipients[2]); + + for (uint32_t i = 0; i < MAX_OF_RECIPIENT_ARRAY; ++i) { + if (originalRecipients[i].IsEmpty()) continue; + + rv = m_compFields->SplitRecipientsEx(originalRecipients[i], + recipientsList[i]); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Then look them up in the Addressbooks + bool stillNeedToSearch = true; + nsCOMPtr<nsIAbDirectory> abDirectory; + nsCOMPtr<nsIAbCard> existingCard; + nsTArray<nsMsgMailList> mailListArray; + nsTArray<nsMsgMailList> mailListProcessed; + + nsCOMArray<nsIAbDirectory> addrbookDirArray; + rv = GetABDirAndMailLists(nsLiteralCString(kAllDirectoryRoot), + addrbookDirArray, mailListArray); + if (NS_FAILED(rv)) return rv; + + nsString dirPath; + uint32_t nbrAddressbook = addrbookDirArray.Count(); + + for (uint32_t k = 0; k < nbrAddressbook && stillNeedToSearch; ++k) { + // Avoid recursive mailing lists. + if (abDirectory && (addrbookDirArray[k] == abDirectory)) { + stillNeedToSearch = false; + break; + } + + abDirectory = addrbookDirArray[k]; + if (!abDirectory) continue; + + stillNeedToSearch = false; + for (uint32_t i = 0; i < MAX_OF_RECIPIENT_ARRAY; i++) { + mailListProcessed.Clear(); + + // Note: We check this each time to allow for length changes. + for (uint32_t j = 0; j < recipientsList[i].Length(); j++) { + nsMsgRecipient& recipient = recipientsList[i][j]; + if (!recipient.mDirectory) { + // First check if it's a mailing list. + size_t index = + mailListArray.IndexOf(recipient, 0, nsMsgMailListComparator()); + if (index != mailListArray.NoIndex && + mailListArray[index].mDirectory) { + // Check mailList Processed. + if (mailListProcessed.Contains(recipient, + nsMsgMailListComparator())) { + // Remove from recipientsList. + recipientsList[i].RemoveElementAt(j--); + continue; + } + + nsCOMPtr<nsIAbDirectory> directory(mailListArray[index].mDirectory); + + // Add mailList to mailListProcessed. + mailListProcessed.AppendElement(directory); + + // Resolve mailList members. + nsTArray<nsMsgRecipient> members; + rv = ResolveMailList(directory, addrbookDirArray, mailListArray, + mailListProcessed, members); + NS_ENSURE_SUCCESS(rv, rv); + + // Remove mailList from recipientsList. + recipientsList[i].RemoveElementAt(j); + + // Merge members into recipientsList[i]. + uint32_t pos = 0; + for (uint32_t c = 0; c < members.Length(); c++) { + nsMsgRecipient& member = members[c]; + if (!recipientsList[i].Contains(member, + nsMsgRecipientComparator())) { + recipientsList[i].InsertElementAt(j + pos, member); + pos++; + } + } + } else { + // Find a card that contains this e-mail address. + rv = abDirectory->CardForEmailAddress( + NS_ConvertUTF16toUTF8(recipient.mEmail), + getter_AddRefs(existingCard)); + if (NS_SUCCEEDED(rv) && existingCard) { + recipient.mCard = existingCard; + recipient.mDirectory = abDirectory; + } else { + stillNeedToSearch = true; + } + } + } + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::ExpandMailingLists() { + RecipientsArray recipientsList; + nsresult rv = LookupAddressBook(recipientsList); + NS_ENSURE_SUCCESS(rv, rv); + + // Reset the final headers with the expanded mailing lists. + nsAutoString recipientsStr; + + for (int i = 0; i < MAX_OF_RECIPIENT_ARRAY; ++i) { + uint32_t nbrRecipients = recipientsList[i].Length(); + if (nbrRecipients == 0) continue; + recipientsStr.Truncate(); + + // Note: We check this each time to allow for length changes. + for (uint32_t j = 0; j < recipientsList[i].Length(); ++j) { + nsMsgRecipient& recipient = recipientsList[i][j]; + + if (!recipientsStr.IsEmpty()) recipientsStr.Append(char16_t(',')); + nsAutoString address; + MakeMimeAddress(recipient.mName, recipient.mEmail, address); + recipientsStr.Append(address); + + if (recipient.mCard) { + bool readOnly; + rv = recipient.mDirectory->GetReadOnly(&readOnly); + NS_ENSURE_SUCCESS(rv, rv); + + // Bump the popularity index for this card since we are about to send + // e-mail to it. + if (!readOnly) { + uint32_t popularityIndex = 0; + if (NS_FAILED(recipient.mCard->GetPropertyAsUint32( + kPopularityIndexProperty, &popularityIndex))) { + // TB 2 wrote the popularity value as hex, so if we get here, + // then we've probably got a hex value. We'll convert it back + // to decimal, as that's the best we can do. + + nsCString hexPopularity; + if (NS_SUCCEEDED(recipient.mCard->GetPropertyAsAUTF8String( + kPopularityIndexProperty, hexPopularity))) { + nsresult errorCode = NS_OK; + popularityIndex = hexPopularity.ToInteger(&errorCode, 16); + if (NS_FAILED(errorCode)) + // We failed, just set it to zero. + popularityIndex = 0; + } else + // We couldn't get it as a string either, so just reset to zero. + popularityIndex = 0; + } + + recipient.mCard->SetPropertyAsUint32(kPopularityIndexProperty, + ++popularityIndex); + recipient.mDirectory->ModifyCard(recipient.mCard); + } + } + } + + switch (i) { + case 0: + m_compFields->SetTo(recipientsStr); + break; + case 1: + m_compFields->SetCc(recipientsStr); + break; + case 2: + m_compFields->SetBcc(recipientsStr); + break; + } + } + + return NS_OK; +} + +/** + * Decides which tags trigger which convertible mode, + * i.e. here is the logic for BodyConvertible. + * Note: Helper function. Parameters are not checked. + */ +void nsMsgCompose::TagConvertible(Element* node, int32_t* _retval) { + *_retval = nsIMsgCompConvertible::No; + + nsAutoString element; + element = node->NodeName(); + + // A style attribute on any element can change layout in any way, + // so that is not convertible. + nsAutoString attribValue; + node->GetAttribute(u"style"_ns, attribValue); + if (!attribValue.IsEmpty()) { + *_retval = nsIMsgCompConvertible::No; + return; + } + + // moz-* classes are used internally by the editor and mail composition + // (like moz-cite-prefix or moz-signature). Those can be discarded. + // But any other ones are unconvertible. Style can be attached to them or any + // other context (e.g. in microformats). + node->GetAttribute(u"class"_ns, attribValue); + if (!attribValue.IsEmpty()) { + if (StringBeginsWith(attribValue, u"moz-"_ns, + nsCaseInsensitiveStringComparator)) { + // We assume that anything with a moz-* class is convertible regardless of + // the tag, because we add, for example, class="moz-signature" to HTML + // messages and we still want to be able to downgrade them. + *_retval = nsIMsgCompConvertible::Plain; + } else { + *_retval = nsIMsgCompConvertible::No; + } + + return; + } + + // ID attributes can contain attached style/context or be target of links + // so we should preserve them. + node->GetAttribute(u"id"_ns, attribValue); + if (!attribValue.IsEmpty()) { + *_retval = nsIMsgCompConvertible::No; + return; + } + + // Alignment is not convertible to plaintext; editor currently uses this. + node->GetAttribute(u"align"_ns, attribValue); + if (!attribValue.IsEmpty()) { + *_retval = nsIMsgCompConvertible::No; + return; + } + + // Title attribute is not convertible to plaintext; + // this also preserves any links with titles. + node->GetAttribute(u"title"_ns, attribValue); + if (!attribValue.IsEmpty()) { + *_retval = nsIMsgCompConvertible::No; + return; + } + + // Treat <font face="monospace"> as converible to plaintext. + if (element.LowerCaseEqualsLiteral("font")) { + node->GetAttribute(u"size"_ns, attribValue); + if (!attribValue.IsEmpty()) { + *_retval = nsIMsgCompConvertible::No; + return; + } + node->GetAttribute(u"face"_ns, attribValue); + if (attribValue.LowerCaseEqualsLiteral("monospace")) { + *_retval = nsIMsgCompConvertible::Plain; + } + } + + if ( // Considered convertible to plaintext: Some "simple" elements + // without non-convertible attributes like style, class, id, + // or align (see above). + element.LowerCaseEqualsLiteral("br") || + element.LowerCaseEqualsLiteral("p") || + element.LowerCaseEqualsLiteral("tt") || + element.LowerCaseEqualsLiteral("html") || + element.LowerCaseEqualsLiteral("head") || + element.LowerCaseEqualsLiteral("meta") || + element.LowerCaseEqualsLiteral("title")) { + *_retval = nsIMsgCompConvertible::Plain; + } else if ( + // element.LowerCaseEqualsLiteral("blockquote") || // see below + element.LowerCaseEqualsLiteral("ul") || + element.LowerCaseEqualsLiteral("ol") || + element.LowerCaseEqualsLiteral("li") || + element.LowerCaseEqualsLiteral("dl") || + element.LowerCaseEqualsLiteral("dt") || + element.LowerCaseEqualsLiteral("dd")) { + *_retval = nsIMsgCompConvertible::Yes; + } else if ( + // element.LowerCaseEqualsLiteral("a") || // see below + element.LowerCaseEqualsLiteral("h1") || + element.LowerCaseEqualsLiteral("h2") || + element.LowerCaseEqualsLiteral("h3") || + element.LowerCaseEqualsLiteral("h4") || + element.LowerCaseEqualsLiteral("h5") || + element.LowerCaseEqualsLiteral("h6") || + element.LowerCaseEqualsLiteral("hr") || + element.LowerCaseEqualsLiteral("pre") || + (mConvertStructs && (element.LowerCaseEqualsLiteral("em") || + element.LowerCaseEqualsLiteral("strong") || + element.LowerCaseEqualsLiteral("code") || + element.LowerCaseEqualsLiteral("b") || + element.LowerCaseEqualsLiteral("i") || + element.LowerCaseEqualsLiteral("u")))) { + *_retval = nsIMsgCompConvertible::Altering; + } else if (element.LowerCaseEqualsLiteral("body")) { + *_retval = nsIMsgCompConvertible::Plain; + + if (node->HasAttribute(u"background"_ns) || // There is a background image + node->HasAttribute( + u"dir"_ns)) { // dir=rtl attributes should not downconvert + *_retval = nsIMsgCompConvertible::No; + } else { + nsAutoString color; + if (node->HasAttribute(u"text"_ns)) { + node->GetAttribute(u"text"_ns, color); + if (!color.EqualsLiteral("#000000")) + *_retval = nsIMsgCompConvertible::Altering; + } + if (*_retval != nsIMsgCompConvertible::Altering && // small optimization + node->HasAttribute(u"bgcolor"_ns)) { + node->GetAttribute(u"bgcolor"_ns, color); + if (!color.LowerCaseEqualsLiteral("#ffffff")) + *_retval = nsIMsgCompConvertible::Altering; + } + } + + // ignore special color setting for link, vlink and alink at this point. + } else if (element.LowerCaseEqualsLiteral("blockquote")) { + // Skip <blockquote type="cite"> + *_retval = nsIMsgCompConvertible::Yes; + + node->GetAttribute(u"type"_ns, attribValue); + if (attribValue.LowerCaseEqualsLiteral("cite")) { + *_retval = nsIMsgCompConvertible::Plain; + } + } else if (element.LowerCaseEqualsLiteral("div") || + element.LowerCaseEqualsLiteral("span") || + element.LowerCaseEqualsLiteral("a")) { + // Do some special checks for these tags. They are inside this |else if| + // for performance reasons. + + // Maybe, it's an <a> element inserted by another recognizer (e.g. 4.x') + if (element.LowerCaseEqualsLiteral("a")) { + // Ignore anchor tag, if the URI is the same as the text + // (as inserted by recognizers). + *_retval = nsIMsgCompConvertible::Altering; + + nsAutoString hrefValue; + node->GetAttribute(u"href"_ns, hrefValue); + nsINodeList* children = node->ChildNodes(); + if (children->Length() > 0) { + nsINode* pItem = children->Item(0); + nsAutoString textValue; + pItem->GetNodeValue(textValue); + if (textValue == hrefValue) *_retval = nsIMsgCompConvertible::Plain; + } + } + + // Lastly, test, if it is just a "simple" <div> or <span> + else if (element.LowerCaseEqualsLiteral("div") || + element.LowerCaseEqualsLiteral("span")) { + *_retval = nsIMsgCompConvertible::Plain; + } + } +} + +/** + * Note: Helper function. Parameters are not checked. + */ +NS_IMETHODIMP +nsMsgCompose::NodeTreeConvertible(Element* node, int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + int32_t result; + + // Check this node + TagConvertible(node, &result); + + // Walk tree recursively to check the children. + nsINodeList* children = node->ChildNodes(); + for (uint32_t i = 0; i < children->Length(); i++) { + nsINode* pItem = children->Item(i); + // We assume all nodes that are not elements are convertible, + // so only test elements. + nsCOMPtr<Element> domElement = do_QueryInterface(pItem); + if (domElement) { + int32_t curresult; + NodeTreeConvertible(domElement, &curresult); + + if (curresult > result) result = curresult; + } + } + + *_retval = result; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::BodyConvertible(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + NS_ENSURE_STATE(m_editor); + + nsCOMPtr<Document> rootDocument; + nsresult rv = m_editor->GetDocument(getter_AddRefs(rootDocument)); + if (NS_FAILED(rv)) return rv; + if (!rootDocument) return NS_ERROR_UNEXPECTED; + + // get the top level element, which contains <html> + nsCOMPtr<Element> rootElement = rootDocument->GetDocumentElement(); + if (!rootElement) return NS_ERROR_UNEXPECTED; + NodeTreeConvertible(rootElement, _retval); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompose::GetIdentity(nsIMsgIdentity** aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + NS_IF_ADDREF(*aIdentity = m_identity); + return NS_OK; +} + +/** + * Position above the quote, that is either <blockquote> or + * <div class="moz-cite-prefix"> or <div class="moz-forward-container"> + * in an inline-forwarded message. + */ +nsresult nsMsgCompose::MoveToAboveQuote(void) { + RefPtr<Element> rootElement; + nsresult rv = m_editor->GetRootElement(getter_AddRefs(rootElement)); + if (NS_FAILED(rv) || !rootElement) { + return rv; + } + + nsCOMPtr<nsINode> node; + nsAutoString attributeName; + nsAutoString attributeValue; + nsAutoString tagLocalName; + attributeName.AssignLiteral("class"); + + RefPtr<nsINode> rootElement2 = rootElement; + node = rootElement2->GetFirstChild(); + while (node) { + nsCOMPtr<Element> element = do_QueryInterface(node); + if (element) { + // First check for <blockquote>. This will most likely not trigger + // since well-behaved quotes are preceded by a cite prefix. + tagLocalName = node->LocalName(); + if (tagLocalName.EqualsLiteral("blockquote")) { + break; + } + + // Get the class value. + element->GetAttribute(attributeName, attributeValue); + + // Now check for the cite prefix, so an element with + // class="moz-cite-prefix". + if (attributeValue.LowerCaseFindASCII("moz-cite-prefix") != kNotFound) { + break; + } + + // Next check for forwarded content. + // The forwarded part is inside an element with + // class="moz-forward-container". + if (attributeValue.LowerCaseFindASCII("moz-forward-container") != + kNotFound) { + break; + } + } + + node = node->GetNextSibling(); + if (!node) { + // No further siblings found, so we didn't find what we were looking for. + rv = NS_OK; + break; + } + } + + // Now position. If no quote was found, we position to the very front. + int32_t offset = 0; + if (node) { + rv = GetChildOffset(node, rootElement2, offset); + if (NS_FAILED(rv)) { + return rv; + } + } + RefPtr<Selection> selection; + m_editor->GetSelection(getter_AddRefs(selection)); + if (selection) rv = selection->CollapseInLimiter(rootElement, offset); + + return rv; +} + +/** + * nsEditor::BeginningOfDocument() will position to the beginning of the + * document before the first editable element. It will position into a + * container. We need to be at the very front. + */ +nsresult nsMsgCompose::MoveToBeginningOfDocument(void) { + RefPtr<Element> rootElement; + nsresult rv = m_editor->GetRootElement(getter_AddRefs(rootElement)); + if (NS_FAILED(rv) || !rootElement) { + return rv; + } + + RefPtr<Selection> selection; + m_editor->GetSelection(getter_AddRefs(selection)); + if (selection) rv = selection->CollapseInLimiter(rootElement, 0); + + return rv; +} + +/** + * M-C's nsEditor::EndOfDocument() will position to the end of the document + * but it will position into a container. We really need to position + * after the last container so we don't accidentally position into a + * <blockquote>. That's why we use our own function. + */ +nsresult nsMsgCompose::MoveToEndOfDocument(void) { + int32_t offset; + RefPtr<Element> rootElement; + nsCOMPtr<nsINode> lastNode; + nsresult rv = m_editor->GetRootElement(getter_AddRefs(rootElement)); + if (NS_FAILED(rv) || !rootElement) { + return rv; + } + + RefPtr<nsINode> rootElement2 = rootElement; + lastNode = rootElement2->GetLastChild(); + if (!lastNode) { + return NS_ERROR_NULL_POINTER; + } + + rv = GetChildOffset(lastNode, rootElement2, offset); + if (NS_FAILED(rv)) { + return rv; + } + + RefPtr<Selection> selection; + m_editor->GetSelection(getter_AddRefs(selection)); + if (selection) rv = selection->CollapseInLimiter(rootElement, offset + 1); + + return rv; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsMsgCompose::SetIdentity(nsIMsgIdentity* aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + + m_identity = aIdentity; + + nsresult rv; + + if (!m_editor) return NS_ERROR_FAILURE; + + RefPtr<Element> rootElement; + rv = m_editor->GetRootElement(getter_AddRefs(rootElement)); + if (NS_FAILED(rv) || !rootElement) return rv; + + // First look for the current signature, if we have one + nsCOMPtr<nsINode> lastNode; + nsCOMPtr<nsINode> node; + nsCOMPtr<nsINode> tempNode; + nsAutoString tagLocalName; + + RefPtr<nsINode> rootElement2 = rootElement; + lastNode = rootElement2->GetLastChild(); + if (lastNode) { + node = lastNode; + // In html, the signature is inside an element with + // class="moz-signature" + bool signatureFound = false; + nsAutoString attributeName; + attributeName.AssignLiteral("class"); + + while (node) { + nsCOMPtr<Element> element = do_QueryInterface(node); + if (element) { + nsAutoString attributeValue; + + element->GetAttribute(attributeName, attributeValue); + + if (attributeValue.LowerCaseFindASCII("moz-signature") != kNotFound) { + signatureFound = true; + break; + } + } + node = node->GetPreviousSibling(); + } + + if (signatureFound) { + nsCOMPtr<nsIEditor> editor(m_editor); // Strong reference. + editor->BeginTransaction(); + tempNode = node->GetPreviousSibling(); + rv = editor->DeleteNode(node); + if (NS_FAILED(rv)) { + editor->EndTransaction(); + return rv; + } + + // Also, remove the <br> right before the signature. + if (tempNode) { + tagLocalName = tempNode->LocalName(); + if (tagLocalName.EqualsLiteral("br")) editor->DeleteNode(tempNode); + } + editor->EndTransaction(); + } + } + + if (!CheckIncludeSignaturePrefs(aIdentity)) return NS_OK; + + // Then add the new one if needed + nsAutoString aSignature; + + // No delimiter needed if not a compose window + bool isQuoted; + switch (mType) { + case nsIMsgCompType::New: + case nsIMsgCompType::NewsPost: + case nsIMsgCompType::MailToUrl: + case nsIMsgCompType::ForwardAsAttachment: + isQuoted = false; + break; + default: + isQuoted = true; + break; + } + + ProcessSignature(aIdentity, isQuoted, &aSignature); + + if (!aSignature.IsEmpty()) { + TranslateLineEnding(aSignature); + nsCOMPtr<nsIEditor> editor(m_editor); // Strong reference. + + editor->BeginTransaction(); + int32_t reply_on_top = 0; + bool sig_bottom = true; + aIdentity->GetReplyOnTop(&reply_on_top); + aIdentity->GetSigBottom(&sig_bottom); + bool sigOnTop = (reply_on_top == 1 && !sig_bottom); + if (sigOnTop && isQuoted) { + rv = MoveToAboveQuote(); + } else { + // Note: New messages aren't quoted so we always move to the end. + rv = MoveToEndOfDocument(); + } + + if (NS_SUCCEEDED(rv)) { + if (m_composeHTML) { + bool oldAllow; + GetAllowRemoteContent(&oldAllow); + SetAllowRemoteContent(true); + rv = MOZ_KnownLive(editor->AsHTMLEditor())->InsertHTML(aSignature); + SetAllowRemoteContent(oldAllow); + } else { + rv = editor->InsertLineBreak(); + InsertDivWrappedTextAtSelection(aSignature, u"moz-signature"_ns); + } + } + editor->EndTransaction(); + } + + return rv; +} + +NS_IMETHODIMP nsMsgCompose::CheckCharsetConversion(nsIMsgIdentity* identity, + char** fallbackCharset, + bool* _retval) { + NS_ENSURE_ARG_POINTER(identity); + NS_ENSURE_ARG_POINTER(_retval); + + // Kept around for legacy reasons. This method is supposed to check that the + // headers can be converted to the appropriate charset, but we don't support + // encoding headers to non-UTF-8, so this is now moot. + if (fallbackCharset) *fallbackCharset = nullptr; + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompose::GetDeliverMode(MSG_DeliverMode* aDeliverMode) { + NS_ENSURE_ARG_POINTER(aDeliverMode); + *aDeliverMode = mDeliverMode; + return NS_OK; +} + +void nsMsgCompose::DeleteTmpAttachments() { + if (mTmpAttachmentsDeleted || m_window) { + // Don't delete tmp attachments if compose window is still open, e.g. saving + // a draft. + return; + } + mTmpAttachmentsDeleted = true; + // Remove temporary attachment files, e.g. key.asc when attaching public key. + nsTArray<RefPtr<nsIMsgAttachment>> attachments; + m_compFields->GetAttachments(attachments); + for (nsIMsgAttachment* attachment : attachments) { + bool isTemporary; + attachment->GetTemporary(&isTemporary); + bool sentViaCloud; + attachment->GetSendViaCloud(&sentViaCloud); + if (isTemporary && !sentViaCloud) { + nsCString url; + attachment->GetUrl(url); + nsCOMPtr<nsIFile> urlFile; + nsresult rv = NS_GetFileFromURLSpec(url, getter_AddRefs(urlFile)); + if (NS_SUCCEEDED(rv)) { + urlFile->Remove(false); + } + } + } +} + +nsMsgMailList::nsMsgMailList(nsIAbDirectory* directory) + : mDirectory(directory) { + mDirectory->GetDirName(mName); + mDirectory->GetDescription(mDescription); + + if (mDescription.IsEmpty()) mDescription = mName; + + mDirectory = directory; +} diff --git a/comm/mailnews/compose/src/nsMsgCompose.h b/comm/mailnews/compose/src/nsMsgCompose.h new file mode 100644 index 0000000000..fe9b9724c8 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCompose.h @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgCompose_H_ +#define _nsMsgCompose_H_ + +#include "nsIMsgCompose.h" +#include "nsCOMArray.h" +#include "nsTObserverArray.h" +#include "nsWeakReference.h" +#include "nsMsgCompFields.h" +#include "nsIOutputStream.h" +#include "nsIMsgQuote.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIBaseWindow.h" +#include "nsIAbDirectory.h" +#include "nsIWebProgressListener.h" +#include "nsIMimeConverter.h" +#include "nsIMsgFolder.h" +#include "mozIDOMWindow.h" +#include "mozilla/dom/Element.h" + +// Forward declares +class QuotingOutputStreamListener; +class nsMsgComposeSendListener; +class nsIEditor; +class nsIArray; +struct nsMsgMailList; + +class nsMsgCompose : public nsIMsgCompose, public nsSupportsWeakReference { + public: + nsMsgCompose(); + + /* this macro defines QueryInterface, AddRef and Release for this class */ + NS_DECL_THREADSAFE_ISUPPORTS + + /*** nsIMsgCompose pure virtual functions */ + NS_DECL_NSIMSGCOMPOSE + + /* nsIMsgSendListener interface */ + NS_DECL_NSIMSGSENDLISTENER + + protected: + virtual ~nsMsgCompose(); + + // Deal with quoting issues... + nsresult QuoteOriginalMessage(); // New template + nsresult SetQuotingToFollow(bool aVal); + nsresult ConvertHTMLToText(nsIFile* aSigFile, nsString& aSigData); + nsresult ConvertTextToHTML(nsIFile* aSigFile, nsString& aSigData); + bool IsEmbeddedObjectSafe(const char* originalScheme, + const char* originalHost, const char* originalPath, + mozilla::dom::Element* element); + nsresult TagEmbeddedObjects(nsIEditor* aEditor); + + nsCString mOriginalMsgURI; // used so we can mark message disposition flags + // after we send the message + + int32_t mWhatHolder; + + nsresult LoadDataFromFile(nsIFile* file, nsString& sigData, + bool aAllowUTF8 = true, bool aAllowUTF16 = true); + + bool CheckIncludeSignaturePrefs(nsIMsgIdentity* identity); + // m_folderName to store the value of the saved drafts folder. + nsCString m_folderName; + MOZ_CAN_RUN_SCRIPT void InsertDivWrappedTextAtSelection( + const nsAString& aText, const nsAString& classStr); + + protected: + nsresult CreateMessage(const nsACString& originalMsgURI, MSG_ComposeType type, + nsIMsgCompFields* compFields); + void CleanUpRecipients(nsString& recipients); + nsresult GetABDirAndMailLists(const nsACString& aDirUri, + nsCOMArray<nsIAbDirectory>& aDirArray, + nsTArray<nsMsgMailList>& aMailListArray); + nsresult ResolveMailList(nsIAbDirectory* aMailList, + nsCOMArray<nsIAbDirectory>& allDirectoriesArray, + nsTArray<nsMsgMailList>& allMailListArray, + nsTArray<nsMsgMailList>& mailListResolved, + nsTArray<nsMsgRecipient>& aListMembers); + void TagConvertible(mozilla::dom::Element* node, int32_t* _retval); + MOZ_CAN_RUN_SCRIPT nsresult MoveToAboveQuote(void); + MOZ_CAN_RUN_SCRIPT nsresult MoveToBeginningOfDocument(void); + MOZ_CAN_RUN_SCRIPT nsresult MoveToEndOfDocument(void); + nsresult ReplaceFileURLs(nsString& sigData); + nsresult DataURLForFileURL(const nsAString& aFileURL, nsAString& aDataURL); + + /** + * Given an nsIFile, attempts to read it into aString. + * + * Note: Use sparingly! This causes main-thread I/O, which causes jank and all + * other bad things. + */ + static nsresult SlurpFileToString(nsIFile* aFile, nsACString& aString); + +// 3 = To, Cc, Bcc +#define MAX_OF_RECIPIENT_ARRAY 3 + typedef nsTArray<nsMsgRecipient> RecipientsArray[MAX_OF_RECIPIENT_ARRAY]; + /** + * This method parses the compose fields and associates email addresses with + * the relevant cards from the address books. + */ + nsresult LookupAddressBook(RecipientsArray& recipientList); + bool IsLastWindow(); + + // Helper function. Parameters are not checked. + bool mConvertStructs; // for TagConvertible + + nsCOMPtr<nsIEditor> m_editor; + mozIDOMWindowProxy* m_window; + nsCOMPtr<nsIDocShell> mDocShell; + nsCOMPtr<nsIBaseWindow> m_baseWindow; + RefPtr<nsMsgCompFields> m_compFields; + nsCOMPtr<nsIMsgIdentity> m_identity; + bool m_composeHTML; + RefPtr<QuotingOutputStreamListener> mQuoteStreamListener; + nsCOMPtr<nsIOutputStream> mBaseStream; + + nsCOMPtr<nsIMsgSend> mMsgSend; // for composition back end + nsCOMPtr<nsIMsgProgress> + mProgress; // use by the back end to report progress to the front end + + // Deal with quoting issues... + nsString mCiteReference; + nsCOMPtr<nsIMsgQuote> mQuote; + bool mQuotingToFollow; // Quoting indicator + MSG_ComposeType mType; // Message type + bool mAutodetectCharset; + bool mDeleteDraft; + nsMsgDispositionState mDraftDisposition; + nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr; + + nsString mSmtpPassword; + nsCString mHtmlToQuote; + + nsTObserverArray<nsCOMPtr<nsIMsgComposeStateListener> > mStateListeners; + nsTObserverArray<nsCOMPtr<nsIMsgSendListener> > mExternalSendListeners; + + bool mAllowRemoteContent; + MSG_DeliverMode mDeliverMode; // nsIMsgCompDeliverMode long. + + friend class QuotingOutputStreamListener; + friend class nsMsgComposeSendListener; + + private: + void DeleteTmpAttachments(); + bool mTmpAttachmentsDeleted; +}; + +//////////////////////////////////////////////////////////////////////////////////// +// THIS IS THE CLASS THAT IS THE STREAM Listener OF THE HTML OUTPUT +// FROM LIBMIME. THIS IS FOR QUOTING +//////////////////////////////////////////////////////////////////////////////////// +class QuotingOutputStreamListener : public nsIMsgQuotingOutputStreamListener, + public nsSupportsWeakReference { + public: + QuotingOutputStreamListener(nsIMsgDBHdr* origMsgHdr, bool quoteHeaders, + bool headersOnly, nsIMsgIdentity* identity, + nsIMsgQuote* msgQuote, bool quoteOriginal, + const nsACString& htmlToQuote); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIMSGQUOTINGOUTPUTSTREAMLISTENER + + nsresult SetComposeObj(nsIMsgCompose* obj); + nsresult ConvertToPlainText(bool formatflowed, bool formatted, + bool disallowBreaks); + MOZ_CAN_RUN_SCRIPT nsresult InsertToCompose(nsIEditor* aEditor, + bool aHTMLEditor); + nsresult AppendToMsgBody(const nsCString& inStr); + + private: + virtual ~QuotingOutputStreamListener(); + nsWeakPtr mWeakComposeObj; + nsString mMsgBody; + nsString mCitePrefix; + nsString mSignature; + bool mQuoteHeaders; + bool mHeadersOnly; + nsCOMPtr<nsIMsgQuote> mQuote; + nsCOMPtr<nsIMimeHeaders> mHeaders; + nsCOMPtr<nsIMsgIdentity> mIdentity; + nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr; + nsString mCiteReference; + nsCOMPtr<nsIMimeConverter> mMimeConverter; + int32_t mUnicodeBufferCharacterLength; + bool mQuoteOriginal; + nsCString mHtmlToQuote; +}; + +//////////////////////////////////////////////////////////////////////////////////// +// This is the listener class for the send operation. We have to create this +// class to listen for message send completion and eventually notify the caller +//////////////////////////////////////////////////////////////////////////////////// +class nsMsgComposeSendListener : public nsIMsgComposeSendListener, + public nsIMsgSendListener, + public nsIMsgCopyServiceListener, + public nsIWebProgressListener { + public: + nsMsgComposeSendListener(void); + + // nsISupports interface + NS_DECL_ISUPPORTS + + // nsIMsgComposeSendListener interface + NS_DECL_NSIMSGCOMPOSESENDLISTENER + + // nsIMsgSendListener interface + NS_DECL_NSIMSGSENDLISTENER + + // nsIMsgCopyServiceListener interface + NS_DECL_NSIMSGCOPYSERVICELISTENER + + // nsIWebProgressListener interface + NS_DECL_NSIWEBPROGRESSLISTENER + + nsresult RemoveDraftOrTemplate(nsIMsgCompose* compObj, nsCString msgURI, + bool isSaveTemplate); + nsresult RemoveCurrentDraftMessage(nsIMsgCompose* compObj, bool calledByCopy, + bool isSaveTemplate); + nsresult GetMsgFolder(nsIMsgCompose* compObj, nsIMsgFolder** msgFolder); + + private: + virtual ~nsMsgComposeSendListener(); + nsWeakPtr mWeakComposeObj; + MSG_DeliverMode mDeliverMode; +}; + +/****************************************************************************** + * nsMsgMailList + ******************************************************************************/ +struct nsMsgMailList { + explicit nsMsgMailList(nsIAbDirectory* directory); + + nsString mName; + nsString mDescription; + nsCOMPtr<nsIAbDirectory> mDirectory; +}; + +#endif /* _nsMsgCompose_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp b/comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp new file mode 100644 index 0000000000..7683d04d89 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeContentHandler.cpp @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgComposeContentHandler.h" +#include "nsMsgComposeService.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "plstr.h" +#include "nsServiceManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsPIDOMWindow.h" +#include "mozIDOMWindow.h" +#include "mozilla/dom/Document.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsNetUtil.h" +#include "nsIMsgFolder.h" +#include "nsIMsgIncomingServer.h" +#include "nsIMsgAccountManager.h" + +#define NS_MSGCOMPOSESERVICE_CID \ + { /* 588595FE-1ADA-11d3-A715-0060B0EB39B5 */ \ + 0x588595fe, 0x1ada, 0x11d3, { \ + 0xa7, 0x15, 0x0, 0x60, 0xb0, 0xeb, 0x39, 0xb5 \ + } \ + } +static NS_DEFINE_CID(kMsgComposeServiceCID, NS_MSGCOMPOSESERVICE_CID); + +nsMsgComposeContentHandler::nsMsgComposeContentHandler() {} + +// The following macro actually implement addref, release and query interface +// for our component. +NS_IMPL_ISUPPORTS(nsMsgComposeContentHandler, nsIContentHandler) + +nsMsgComposeContentHandler::~nsMsgComposeContentHandler() {} + +// Try to get an appropriate nsIMsgIdentity by going through the window, getting +// the document's URI, then the corresponding nsIMsgDBHdr. Then find the server +// associated with that header and get the first identity for it. +nsresult nsMsgComposeContentHandler::GetBestIdentity( + nsIInterfaceRequestor* aWindowContext, nsIMsgIdentity** aIdentity) { + nsresult rv; + + nsCOMPtr<mozIDOMWindowProxy> domWindow = do_GetInterface(aWindowContext); + NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE); + nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(domWindow); + + nsAutoString documentURIString; + rv = window->GetDoc()->GetDocumentURI(documentURIString); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> documentURI; + rv = NS_NewURI(getter_AddRefs(documentURI), documentURIString); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgMessageUrl> msgURI = do_QueryInterface(documentURI); + if (!msgURI) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = msgURI->GetMessageHeader(getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> folder; + rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + // nsIMsgDBHdrs from .eml messages have a null folder, so bail out if that's + // the case. + if (!folder) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = accountManager->GetFirstIdentityForServer(server, aIdentity); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +NS_IMETHODIMP nsMsgComposeContentHandler::HandleContent( + const char* aContentType, nsIInterfaceRequestor* aWindowContext, + nsIRequest* request) { + nsresult rv = NS_OK; + if (!request) return NS_ERROR_NULL_POINTER; + + // First of all, get the content type and make sure it is a content type we + // know how to handle! + if (PL_strcasecmp(aContentType, "application/x-mailto") == 0) { + nsCOMPtr<nsIMsgIdentity> identity; + + if (aWindowContext) + GetBestIdentity(aWindowContext, getter_AddRefs(identity)); + + nsCOMPtr<nsIURI> aUri; + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) return NS_ERROR_FAILURE; + + rv = aChannel->GetURI(getter_AddRefs(aUri)); + if (aUri) { + nsCOMPtr<nsIMsgComposeService> composeService = + do_GetService(kMsgComposeServiceCID, &rv); + if (NS_SUCCEEDED(rv)) + rv = composeService->OpenComposeWindowWithURI(nullptr, aUri, identity); + } + } else { + // The content-type was not application/x-mailto... + return NS_ERROR_WONT_HANDLE_CONTENT; + } + + return rv; +} diff --git a/comm/mailnews/compose/src/nsMsgComposeContentHandler.h b/comm/mailnews/compose/src/nsMsgComposeContentHandler.h new file mode 100644 index 0000000000..158edfa09b --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeContentHandler.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIContentHandler.h" +#include "nsIMsgIdentity.h" + +class nsMsgComposeContentHandler : public nsIContentHandler { + public: + nsMsgComposeContentHandler(); + + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTHANDLER + private: + virtual ~nsMsgComposeContentHandler(); + nsresult GetBestIdentity(nsIInterfaceRequestor* aWindowContext, + nsIMsgIdentity** identity); +}; diff --git a/comm/mailnews/compose/src/nsMsgComposeParams.cpp b/comm/mailnews/compose/src/nsMsgComposeParams.cpp new file mode 100644 index 0000000000..653ed11f46 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeParams.cpp @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgComposeParams.h" + +nsMsgComposeParams::nsMsgComposeParams() + : mType(nsIMsgCompType::New), + mFormat(nsIMsgCompFormat::Default), + mBodyIsLink(false), + mAutodetectCharset(false) {} + +/* the following macro actually implement addref, release and query interface + * for our component. */ +NS_IMPL_ISUPPORTS(nsMsgComposeParams, nsIMsgComposeParams) + +nsMsgComposeParams::~nsMsgComposeParams() {} + +/* attribute MSG_ComposeType type; */ +NS_IMETHODIMP nsMsgComposeParams::GetType(MSG_ComposeType* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = mType; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetType(MSG_ComposeType aType) { + mType = aType; + return NS_OK; +} + +/* attribute MSG_ComposeFormat format; */ +NS_IMETHODIMP nsMsgComposeParams::GetFormat(MSG_ComposeFormat* aFormat) { + NS_ENSURE_ARG_POINTER(aFormat); + + *aFormat = mFormat; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetFormat(MSG_ComposeFormat aFormat) { + mFormat = aFormat; + return NS_OK; +} + +/* attribute string originalMsgURI; */ +NS_IMETHODIMP nsMsgComposeParams::GetOriginalMsgURI( + nsACString& aOriginalMsgURI) { + aOriginalMsgURI = mOriginalMsgUri; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetOriginalMsgURI( + const nsACString& aOriginalMsgURI) { + mOriginalMsgUri = aOriginalMsgURI; + return NS_OK; +} + +/* attribute nsIMsgIdentity identity; */ +NS_IMETHODIMP nsMsgComposeParams::GetIdentity(nsIMsgIdentity** aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + NS_IF_ADDREF(*aIdentity = mIdentity); + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeParams::SetIdentity(nsIMsgIdentity* aIdentity) { + mIdentity = aIdentity; + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeParams::SetOrigMsgHdr(nsIMsgDBHdr* aMsgHdr) { + mOrigMsgHdr = aMsgHdr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeParams::GetOrigMsgHdr(nsIMsgDBHdr** aMsgHdr) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_IF_ADDREF(*aMsgHdr = mOrigMsgHdr); + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeParams::GetAutodetectCharset( + bool* aAutodetectCharset) { + NS_ENSURE_ARG_POINTER(aAutodetectCharset); + *aAutodetectCharset = mAutodetectCharset; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetAutodetectCharset( + bool aAutodetectCharset) { + mAutodetectCharset = aAutodetectCharset; + return NS_OK; +} + +/* attribute ACString htmlToQuote; */ +NS_IMETHODIMP nsMsgComposeParams::GetHtmlToQuote(nsACString& aHtmlToQuote) { + aHtmlToQuote = mHtmlToQuote; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetHtmlToQuote( + const nsACString& aHtmlToQuote) { + mHtmlToQuote = aHtmlToQuote; + return NS_OK; +} + +/* attribute nsIMsgCompFields composeFields; */ +NS_IMETHODIMP nsMsgComposeParams::GetComposeFields( + nsIMsgCompFields** aComposeFields) { + NS_ENSURE_ARG_POINTER(aComposeFields); + + if (mComposeFields) { + NS_ADDREF(*aComposeFields = mComposeFields); + } else + *aComposeFields = nullptr; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetComposeFields( + nsIMsgCompFields* aComposeFields) { + mComposeFields = aComposeFields; + return NS_OK; +} + +/* attribute boolean bodyIsLink; */ +NS_IMETHODIMP nsMsgComposeParams::GetBodyIsLink(bool* aBodyIsLink) { + NS_ENSURE_ARG_POINTER(aBodyIsLink); + + *aBodyIsLink = mBodyIsLink; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetBodyIsLink(bool aBodyIsLink) { + mBodyIsLink = aBodyIsLink; + return NS_OK; +} + +/* attribute nsIMsgSendLisneter sendListener; */ +NS_IMETHODIMP nsMsgComposeParams::GetSendListener( + nsIMsgSendListener** aSendListener) { + NS_ENSURE_ARG_POINTER(aSendListener); + + if (mSendListener) { + NS_ADDREF(*aSendListener = mSendListener); + } else + *aSendListener = nullptr; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetSendListener( + nsIMsgSendListener* aSendListener) { + mSendListener = aSendListener; + return NS_OK; +} + +/* attribute string smtpPassword; */ +NS_IMETHODIMP nsMsgComposeParams::GetSmtpPassword(nsAString& aSmtpPassword) { + aSmtpPassword = mSMTPPassword; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeParams::SetSmtpPassword( + const nsAString& aSmtpPassword) { + mSMTPPassword = aSmtpPassword; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgComposeParams.h b/comm/mailnews/compose/src/nsMsgComposeParams.h new file mode 100644 index 0000000000..b447304e30 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeParams.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIMsgComposeParams.h" +#include "nsString.h" +#include "nsIMsgHdr.h" +#include "nsCOMPtr.h" +class nsMsgComposeParams : public nsIMsgComposeParams { + public: + nsMsgComposeParams(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGCOMPOSEPARAMS + + private: + virtual ~nsMsgComposeParams(); + MSG_ComposeType mType; + MSG_ComposeFormat mFormat; + nsCString mOriginalMsgUri; + nsCOMPtr<nsIMsgIdentity> mIdentity; + nsCOMPtr<nsIMsgCompFields> mComposeFields; + bool mBodyIsLink; + nsCOMPtr<nsIMsgSendListener> mSendListener; + nsString mSMTPPassword; + nsCOMPtr<nsIMsgDBHdr> mOrigMsgHdr; + bool mAutodetectCharset; + nsCString mHtmlToQuote; +}; diff --git a/comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp b/comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp new file mode 100644 index 0000000000..d54355ab8f --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeProgressParams.cpp @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgComposeProgressParams.h" +#include "nsServiceManagerUtils.h" + +NS_IMPL_ISUPPORTS(nsMsgComposeProgressParams, nsIMsgComposeProgressParams) + +nsMsgComposeProgressParams::nsMsgComposeProgressParams() + : m_deliveryMode(nsIMsgCompDeliverMode::Now) {} + +nsMsgComposeProgressParams::~nsMsgComposeProgressParams() {} + +/* attribute wstring subject; */ +NS_IMETHODIMP nsMsgComposeProgressParams::GetSubject(char16_t** aSubject) { + NS_ENSURE_ARG(aSubject); + + *aSubject = ToNewUnicode(m_subject); + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeProgressParams::SetSubject(const char16_t* aSubject) { + m_subject = aSubject; + return NS_OK; +} + +/* attribute MSG_DeliverMode deliveryMode; */ +NS_IMETHODIMP nsMsgComposeProgressParams::GetDeliveryMode( + MSG_DeliverMode* aDeliveryMode) { + NS_ENSURE_ARG(aDeliveryMode); + + *aDeliveryMode = m_deliveryMode; + return NS_OK; +} +NS_IMETHODIMP nsMsgComposeProgressParams::SetDeliveryMode( + MSG_DeliverMode aDeliveryMode) { + m_deliveryMode = aDeliveryMode; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgComposeProgressParams.h b/comm/mailnews/compose/src/nsMsgComposeProgressParams.h new file mode 100644 index 0000000000..141d870195 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeProgressParams.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIMsgComposeProgressParams.h" + +class nsMsgComposeProgressParams : public nsIMsgComposeProgressParams { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGCOMPOSEPROGRESSPARAMS + + nsMsgComposeProgressParams(); + + private: + virtual ~nsMsgComposeProgressParams(); + nsString m_subject; + MSG_DeliverMode m_deliveryMode; +}; diff --git a/comm/mailnews/compose/src/nsMsgComposeService.cpp b/comm/mailnews/compose/src/nsMsgComposeService.cpp new file mode 100644 index 0000000000..be12fbbef4 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeService.cpp @@ -0,0 +1,1411 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgComposeService.h" +#include "nsIMsgSend.h" +#include "nsIServiceManager.h" +#include "nsIObserverService.h" +#include "nsIMsgIdentity.h" +#include "nsISmtpUrl.h" +#include "nsIURI.h" +#include "nsMsgI18N.h" +#include "nsIMsgComposeParams.h" +#include "nsXPCOM.h" +#include "nsISupportsPrimitives.h" +#include "nsIWindowWatcher.h" +#include "mozIDOMWindow.h" +#include "nsIContentViewer.h" +#include "nsIMsgWindow.h" +#include "nsIDocShell.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/Document.h" +#include "nsIAppWindow.h" +#include "nsIWindowMediator.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIBaseWindow.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIMsgAccountManager.h" +#include "nsIStreamConverter.h" +#include "nsToolkitCompsCID.h" +#include "nsNetUtil.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIMsgDatabase.h" +#include "nsIDocumentEncoder.h" +#include "nsContentCID.h" +#include "mozilla/dom/Selection.h" +#include "nsUTF8Utils.h" +#include "mozilla/intl/LineBreaker.h" +#include "mimemoz2.h" +#include "nsIURIMutator.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/XULFrameElement.h" +#include "nsFrameLoader.h" +#include "nsSmtpUrl.h" +#include "nsUnicharUtils.h" +#include "mozilla/NullPrincipal.h" + +#ifdef MSGCOMP_TRACE_PERFORMANCE +# include "mozilla/Logging.h" +# include "nsIMsgHdr.h" +# include "nsIMsgMessageService.h" +# include "nsMsgUtils.h" +#endif + +#include "nsICommandLine.h" +#include "nsIAppStartup.h" +#include "nsMsgUtils.h" +#include "nsIPrincipal.h" +#include "nsIMutableArray.h" + +using namespace mozilla; +using namespace mozilla::dom; + +#ifdef XP_WIN +# include <windows.h> +# include <shellapi.h> +# include "nsIWidget.h" +#endif + +#define DEFAULT_CHROME \ + "chrome://messenger/content/messengercompose/messengercompose.xhtml"_ns + +#define PREF_MAILNEWS_REPLY_QUOTING_SELECTION "mailnews.reply_quoting_selection" +#define PREF_MAILNEWS_REPLY_QUOTING_SELECTION_MULTI_WORD \ + "mailnews.reply_quoting_selection.multi_word" +#define PREF_MAILNEWS_REPLY_QUOTING_SELECTION_ONLY_IF \ + "mailnews.reply_quoting_selection.only_if_chars" + +#define MAIL_ROOT_PREF "mail." +#define MAILNEWS_ROOT_PREF "mailnews." +#define HTMLDOMAINUPDATE_VERSION_PREF_NAME "global_html_domains.version" +#define HTMLDOMAINUPDATE_DOMAINLIST_PREF_NAME "global_html_domains" +#define USER_CURRENT_HTMLDOMAINLIST_PREF_NAME "html_domains" +#define USER_CURRENT_PLAINTEXTDOMAINLIST_PREF_NAME "plaintext_domains" +#define DOMAIN_DELIMITER ',' + +#ifdef MSGCOMP_TRACE_PERFORMANCE +static mozilla::LazyLogModule MsgComposeLogModule("MsgCompose"); + +static uint32_t GetMessageSizeFromURI(const nsACString& originalMsgURI) { + uint32_t msgSize = 0; + + if (!originalMsgURI.IsEmpty()) { + nsCOMPtr<nsIMsgDBHdr> originalMsgHdr; + GetMsgDBHdrFromURI(originalMsgURI, getter_AddRefs(originalMsgHdr)); + if (originalMsgHdr) originalMsgHdr->GetMessageSize(&msgSize); + } + + return msgSize; +} +#endif + +nsMsgComposeService::nsMsgComposeService() { + // Defaulting the value of mLogComposePerformance to FALSE to prevent logging. + mLogComposePerformance = false; +#ifdef MSGCOMP_TRACE_PERFORMANCE + mStartTime = PR_IntervalNow(); + mPreviousTime = mStartTime; +#endif +} + +NS_IMPL_ISUPPORTS(nsMsgComposeService, nsIMsgComposeService, + ICOMMANDLINEHANDLER, nsISupportsWeakReference) + +nsMsgComposeService::~nsMsgComposeService() { mOpenComposeWindows.Clear(); } + +nsresult nsMsgComposeService::Init() { + nsresult rv = NS_OK; + + Reset(); + + AddGlobalHtmlDomains(); + // Since the compose service should only be initialized once, we can + // be pretty sure there aren't any existing compose windows open. + MsgCleanupTempFiles("nsmail", "tmp"); + MsgCleanupTempFiles("nscopy", "tmp"); + MsgCleanupTempFiles("nsemail", "eml"); + MsgCleanupTempFiles("nsemail", "tmp"); + MsgCleanupTempFiles("nsqmail", "tmp"); + return rv; +} + +void nsMsgComposeService::Reset() { + mOpenComposeWindows.Clear(); + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) + prefs->GetBoolPref("mailnews.logComposePerformance", + &mLogComposePerformance); +} + +// Function to open a message compose window and pass an nsIMsgComposeParams +// parameter to it. +NS_IMETHODIMP +nsMsgComposeService::OpenComposeWindowWithParams(const char* chrome, + nsIMsgComposeParams* params) { + NS_ENSURE_ARG_POINTER(params); +#ifdef MSGCOMP_TRACE_PERFORMANCE + if (mLogComposePerformance) { + TimeStamp("Start opening the window", true); + } +#endif + + nsresult rv; + + NS_ENSURE_ARG_POINTER(params); + + // Use default identity if no identity has been specified + nsCOMPtr<nsIMsgIdentity> identity; + params->GetIdentity(getter_AddRefs(identity)); + if (!identity) { + GetDefaultIdentity(getter_AddRefs(identity)); + params->SetIdentity(identity); + } + + // Create a new window. + nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + if (!wwatch) return NS_ERROR_FAILURE; + + nsCOMPtr<nsISupportsInterfacePointer> msgParamsWrapper = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + msgParamsWrapper->SetData(params); + msgParamsWrapper->SetDataIID(&NS_GET_IID(nsIMsgComposeParams)); + + nsCOMPtr<mozIDOMWindowProxy> newWindow; + nsAutoCString chromeURL; + if (chrome && *chrome) { + chromeURL = nsDependentCString(chrome); + } else { + chromeURL = DEFAULT_CHROME; + } + rv = wwatch->OpenWindow(0, chromeURL, "_blank"_ns, + "all,chrome,dialog=no,status,toolbar"_ns, + msgParamsWrapper, getter_AddRefs(newWindow)); + + return rv; +} + +NS_IMETHODIMP +nsMsgComposeService::DetermineComposeHTML(nsIMsgIdentity* aIdentity, + MSG_ComposeFormat aFormat, + bool* aComposeHTML) { + NS_ENSURE_ARG_POINTER(aComposeHTML); + + *aComposeHTML = true; + switch (aFormat) { + case nsIMsgCompFormat::HTML: + *aComposeHTML = true; + break; + case nsIMsgCompFormat::PlainText: + *aComposeHTML = false; + break; + + default: + nsCOMPtr<nsIMsgIdentity> identity = aIdentity; + if (!identity) GetDefaultIdentity(getter_AddRefs(identity)); + + if (identity) { + identity->GetComposeHtml(aComposeHTML); + if (aFormat == nsIMsgCompFormat::OppositeOfDefault) + *aComposeHTML = !*aComposeHTML; + } else { + // default identity not found. Use the mail.html_compose pref to + // determine message compose type (HTML or PlainText). + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + nsresult rv; + bool useHTMLCompose; + rv = prefs->GetBoolPref(MAIL_ROOT_PREF "html_compose", + &useHTMLCompose); + if (NS_SUCCEEDED(rv)) *aComposeHTML = useHTMLCompose; + } + } + break; + } + + return NS_OK; +} + +MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION nsresult +nsMsgComposeService::GetOrigWindowSelection(MSG_ComposeType type, + mozilla::dom::Selection* selection, + nsACString& aSelHTML) { + nsresult rv; + + // Good hygiene + aSelHTML.Truncate(); + + // Get the pref to see if we even should do reply quoting selection + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool replyQuotingSelection; + rv = prefs->GetBoolPref(PREF_MAILNEWS_REPLY_QUOTING_SELECTION, + &replyQuotingSelection); + NS_ENSURE_SUCCESS(rv, rv); + if (!replyQuotingSelection) return NS_ERROR_ABORT; + + bool requireMultipleWords = true; + nsAutoCString charsOnlyIf; + prefs->GetBoolPref(PREF_MAILNEWS_REPLY_QUOTING_SELECTION_MULTI_WORD, + &requireMultipleWords); + prefs->GetCharPref(PREF_MAILNEWS_REPLY_QUOTING_SELECTION_ONLY_IF, + charsOnlyIf); + if (requireMultipleWords || !charsOnlyIf.IsEmpty()) { + nsAutoString selPlain; + selection->Stringify(selPlain); + + // If "mailnews.reply_quoting_selection.multi_word" is on, then there must + // be at least two words selected in order to quote just the selected text + if (requireMultipleWords) { + if (selPlain.IsEmpty()) return NS_ERROR_ABORT; + + if (NS_SUCCEEDED(rv)) { + const uint32_t length = selPlain.Length(); + const char16_t* unicodeStr = selPlain.get(); + int32_t endWordPos = + mozilla::intl::LineBreaker::Next(unicodeStr, length, 0); + + // If there's not even one word, then there's not multiple words + if (endWordPos == NS_LINEBREAKER_NEED_MORE_TEXT) return NS_ERROR_ABORT; + + // If after the first word is only space, then there's not multiple + // words + const char16_t* end; + for (end = unicodeStr + endWordPos; mozilla::intl::NS_IsSpace(*end); + end++) + ; + if (!*end) return NS_ERROR_ABORT; + } + } + + if (!charsOnlyIf.IsEmpty()) { + if (selPlain.FindCharInSet(NS_ConvertUTF8toUTF16(charsOnlyIf)) == + kNotFound) { + return NS_ERROR_ABORT; + } + } + } + + nsAutoString selHTML; + IgnoredErrorResult rv2; + selection->ToStringWithFormat(u"text/html"_ns, + nsIDocumentEncoder::SkipInvisibleContent, 0, + selHTML, rv2); + if (rv2.Failed()) { + return NS_ERROR_FAILURE; + } + + // Now remove <span class="moz-txt-citetags">> </span>. + nsAutoCString html(NS_ConvertUTF16toUTF8(selHTML).get()); + int32_t spanInd = html.Find("<span class=\"moz-txt-citetags\">"); + while (spanInd != kNotFound) { + nsAutoCString right0(Substring(html, spanInd)); + int32_t endInd = right0.Find("</span>"); + if (endInd == kNotFound) break; // oops, where is the closing tag gone? + nsAutoCString right1(Substring(html, spanInd + endInd + 7)); + html.SetLength(spanInd); + html.Append(right1); + spanInd = html.Find("<span class=\"moz-txt-citetags\">"); + } + + aSelHTML.Assign(html); + + return rv; +} + +MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION NS_IMETHODIMP +nsMsgComposeService::OpenComposeWindow( + const nsACString& msgComposeWindowURL, nsIMsgDBHdr* origMsgHdr, + const nsACString& originalMsgURI, MSG_ComposeType type, + MSG_ComposeFormat format, nsIMsgIdentity* aIdentity, const nsACString& from, + nsIMsgWindow* aMsgWindow, mozilla::dom::Selection* selection, + bool autodetectCharset) { + nsresult rv; + + nsCOMPtr<nsIMsgIdentity> identity = aIdentity; + if (!identity) GetDefaultIdentity(getter_AddRefs(identity)); + + /* Actually, the only way to implement forward inline is to simulate a + template message. Maybe one day when we will have more time we can change + that + */ + if (type == nsIMsgCompType::ForwardInline || type == nsIMsgCompType::Draft || + type == nsIMsgCompType::EditTemplate || + type == nsIMsgCompType::Template || + type == nsIMsgCompType::ReplyWithTemplate || + type == nsIMsgCompType::Redirect || type == nsIMsgCompType::EditAsNew) { + nsAutoCString uriToOpen(originalMsgURI); + char sep = (uriToOpen.FindChar('?') == kNotFound) ? '?' : '&'; + + // The compose type that gets transmitted to a compose window open in mime + // is communicated using url query parameters here. + if (type == nsIMsgCompType::Redirect) { + uriToOpen += sep; + uriToOpen.AppendLiteral("redirect=true"); + } else if (type == nsIMsgCompType::EditAsNew) { + uriToOpen += sep; + uriToOpen.AppendLiteral("editasnew=true"); + } else if (type == nsIMsgCompType::EditTemplate) { + uriToOpen += sep; + uriToOpen.AppendLiteral("edittempl=true"); + } + + return LoadDraftOrTemplate( + uriToOpen, + type == nsIMsgCompType::ForwardInline || type == nsIMsgCompType::Draft + ? nsMimeOutput::nsMimeMessageDraftOrTemplate + : nsMimeOutput::nsMimeMessageEditorTemplate, + identity, originalMsgURI, origMsgHdr, + type == nsIMsgCompType::ForwardInline, + format == nsIMsgCompFormat::OppositeOfDefault, aMsgWindow, + autodetectCharset); + } + + nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams( + do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv)); + if (NS_SUCCEEDED(rv) && pMsgComposeParams) { + nsCOMPtr<nsIMsgCompFields> pMsgCompFields(do_CreateInstance( + "@mozilla.org/messengercompose/composefields;1", &rv)); + if (NS_SUCCEEDED(rv) && pMsgCompFields) { + pMsgComposeParams->SetType(type); + pMsgComposeParams->SetFormat(format); + pMsgComposeParams->SetIdentity(identity); + pMsgComposeParams->SetAutodetectCharset(autodetectCharset); + + // When doing a reply (except with a template) see if there's a selection + // that we should quote + if (selection && + (type == nsIMsgCompType::Reply || type == nsIMsgCompType::ReplyAll || + type == nsIMsgCompType::ReplyToSender || + type == nsIMsgCompType::ReplyToGroup || + type == nsIMsgCompType::ReplyToSenderAndGroup || + type == nsIMsgCompType::ReplyToList)) { + nsAutoCString selHTML; + if (NS_SUCCEEDED(GetOrigWindowSelection(type, selection, selHTML))) { + nsCOMPtr<nsINode> node = selection->GetFocusNode(); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + IgnoredErrorResult er; + + if ((node->LocalName().IsEmpty() || + node->LocalName().EqualsLiteral("pre")) && + node->OwnerDoc()->QuerySelector( + "body > div:first-of-type.moz-text-plain"_ns, er)) { + // Treat the quote as <pre> for selections in moz-text-plain bodies. + // If focusNode.localName isn't empty, we had e.g. body selected + // and should not add <pre>. + pMsgComposeParams->SetHtmlToQuote("<pre>"_ns + selHTML + + "</pre>"_ns); + } else { + pMsgComposeParams->SetHtmlToQuote(selHTML); + } + } + } + + if (!originalMsgURI.IsEmpty()) { + if (type == nsIMsgCompType::NewsPost) { + nsAutoCString newsURI(originalMsgURI); + nsAutoCString group; + nsAutoCString host; + + int32_t slashpos = newsURI.RFindChar('/'); + if (slashpos > 0) { + // uri is "[s]news://host[:port]/group" + host = StringHead(newsURI, slashpos); + group = Substring(newsURI, slashpos + 1); + + } else + group = originalMsgURI; + + nsAutoCString unescapedName; + MsgUnescapeString(group, + nsINetUtil::ESCAPE_URL_FILE_BASENAME | + nsINetUtil::ESCAPE_URL_FORCED, + unescapedName); + pMsgCompFields->SetNewsgroups(NS_ConvertUTF8toUTF16(unescapedName)); + pMsgCompFields->SetNewspostUrl(host.get()); + } else { + pMsgComposeParams->SetOriginalMsgURI(originalMsgURI); + pMsgComposeParams->SetOrigMsgHdr(origMsgHdr); + pMsgCompFields->SetFrom(NS_ConvertUTF8toUTF16(from)); + } + } + + pMsgComposeParams->SetComposeFields(pMsgCompFields); + + if (mLogComposePerformance) { +#ifdef MSGCOMP_TRACE_PERFORMANCE + // ducarroz, properly fix this in the case of new message (not a reply) + if (type != nsIMsgCompType::NewsPost) { + char buff[256]; + sprintf(buff, "Start opening the window, message size = %d", + GetMessageSizeFromURI(originalMsgURI)); + TimeStamp(buff, true); + } +#endif + } // end if(mLogComposePerformance) + + rv = OpenComposeWindowWithParams( + PromiseFlatCString(msgComposeWindowURL).get(), pMsgComposeParams); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgComposeService::GetParamsForMailto( + nsIURI* aURI, nsIMsgComposeParams** aParams) { + nsresult rv = NS_OK; + if (aURI) { + nsCString spec; + aURI->GetSpec(spec); + + nsCOMPtr<nsIURI> url; + rv = nsMailtoUrl::NewMailtoURI(spec, nullptr, getter_AddRefs(url)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMailtoUrl> aMailtoUrl = do_QueryInterface(url, &rv); + + if (NS_SUCCEEDED(rv)) { + MSG_ComposeFormat requestedComposeFormat = nsIMsgCompFormat::Default; + nsCString toPart; + nsCString ccPart; + nsCString bccPart; + nsCString subjectPart; + nsCString bodyPart; + nsCString newsgroup; + nsCString refPart; + nsCString HTMLBodyPart; + + aMailtoUrl->GetMessageContents(toPart, ccPart, bccPart, subjectPart, + bodyPart, HTMLBodyPart, refPart, newsgroup, + &requestedComposeFormat); + + nsAutoString sanitizedBody; + + bool composeHTMLFormat; + DetermineComposeHTML(NULL, requestedComposeFormat, &composeHTMLFormat); + + // If there was an 'html-body' param, finding it will have requested + // HTML format in GetMessageContents, so we try to use it first. If it's + // empty, but we are composing in HTML because of the user's prefs, the + // 'body' param needs to be escaped, since it's supposed to be plain + // text, but it then doesn't need to sanitized. + nsString rawBody; + if (HTMLBodyPart.IsEmpty()) { + if (composeHTMLFormat) { + nsCString escaped; + nsAppendEscapedHTML(bodyPart, escaped); + CopyUTF8toUTF16(escaped, sanitizedBody); + } else + CopyUTF8toUTF16(bodyPart, rawBody); + } else + CopyUTF8toUTF16(HTMLBodyPart, rawBody); + + if (!rawBody.IsEmpty() && composeHTMLFormat) { + // For security reason, we must sanitize the message body before + // accepting any html... + + rv = HTMLSanitize(rawBody, sanitizedBody); // from mimemoz2.h + + if (NS_FAILED(rv)) { + // Something went horribly wrong with parsing for html format + // in the body. Set composeHTMLFormat to false so we show the + // plain text mail compose. + composeHTMLFormat = false; + } + } + + nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams(do_CreateInstance( + "@mozilla.org/messengercompose/composeparams;1", &rv)); + if (NS_SUCCEEDED(rv) && pMsgComposeParams) { + pMsgComposeParams->SetType(nsIMsgCompType::MailToUrl); + pMsgComposeParams->SetFormat(composeHTMLFormat + ? nsIMsgCompFormat::HTML + : nsIMsgCompFormat::PlainText); + + nsCOMPtr<nsIMsgCompFields> pMsgCompFields(do_CreateInstance( + "@mozilla.org/messengercompose/composefields;1", &rv)); + if (pMsgCompFields) { + // ugghh more conversion work!!!! + pMsgCompFields->SetTo(NS_ConvertUTF8toUTF16(toPart)); + pMsgCompFields->SetCc(NS_ConvertUTF8toUTF16(ccPart)); + pMsgCompFields->SetBcc(NS_ConvertUTF8toUTF16(bccPart)); + pMsgCompFields->SetNewsgroups(NS_ConvertUTF8toUTF16(newsgroup)); + pMsgCompFields->SetReferences(refPart.get()); + pMsgCompFields->SetSubject(NS_ConvertUTF8toUTF16(subjectPart)); + pMsgCompFields->SetBody(composeHTMLFormat ? sanitizedBody : rawBody); + pMsgComposeParams->SetComposeFields(pMsgCompFields); + + NS_ADDREF(*aParams = pMsgComposeParams); + return NS_OK; + } + } // if we created msg compose params.... + } // if we had a mailto url + } // if we had a url... + + // if we got here we must have encountered an error + *aParams = nullptr; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgComposeService::OpenComposeWindowWithURI( + const char* aMsgComposeWindowURL, nsIURI* aURI, nsIMsgIdentity* identity) { + nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams; + nsresult rv = GetParamsForMailto(aURI, getter_AddRefs(pMsgComposeParams)); + if (NS_SUCCEEDED(rv)) { + pMsgComposeParams->SetIdentity(identity); + rv = OpenComposeWindowWithParams(aMsgComposeWindowURL, pMsgComposeParams); + } + return rv; +} + +NS_IMETHODIMP nsMsgComposeService::InitCompose(nsIMsgComposeParams* aParams, + mozIDOMWindowProxy* aWindow, + nsIDocShell* aDocShell, + nsIMsgCompose** _retval) { + nsresult rv; + nsCOMPtr<nsIMsgCompose> msgCompose = + do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = msgCompose->Initialize(aParams, aWindow, aDocShell); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = msgCompose); + return rv; +} + +NS_IMETHODIMP +nsMsgComposeService::GetDefaultIdentity(nsIMsgIdentity** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgAccount> defaultAccount; + rv = accountManager->GetDefaultAccount(getter_AddRefs(defaultAccount)); + NS_ENSURE_SUCCESS(rv, rv); + + return defaultAccount ? defaultAccount->GetDefaultIdentity(_retval) : NS_OK; +} + +/* readonly attribute boolean logComposePerformance; */ +NS_IMETHODIMP nsMsgComposeService::GetLogComposePerformance( + bool* aLogComposePerformance) { + *aLogComposePerformance = mLogComposePerformance; + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeService::TimeStamp(const char* label, + bool resetTime) { + if (!mLogComposePerformance) return NS_OK; + +#ifdef MSGCOMP_TRACE_PERFORMANCE + + PRIntervalTime now; + + if (resetTime) { + MOZ_LOG(MsgComposeLogModule, mozilla::LogLevel::Info, + ("\n[process]: [totalTime][deltaTime]\n--------------------\n")); + + mStartTime = PR_IntervalNow(); + mPreviousTime = mStartTime; + now = mStartTime; + } else + now = PR_IntervalNow(); + + PRIntervalTime totalTime = PR_IntervalToMilliseconds(now - mStartTime); + PRIntervalTime deltaTime = PR_IntervalToMilliseconds(now - mPreviousTime); + + MOZ_LOG(MsgComposeLogModule, mozilla::LogLevel::Info, + ("[%3.2f][%3.2f] - %s\n", ((double)totalTime / 1000.0) + 0.005, + ((double)deltaTime / 1000.0) + 0.005, label)); + + mPreviousTime = now; +#endif + return NS_OK; +} + +class nsMsgTemplateReplyHelper final : public nsIStreamListener, + public nsIUrlListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + nsMsgTemplateReplyHelper(); + + nsCOMPtr<nsIMsgDBHdr> mHdrToReplyTo; + nsCOMPtr<nsIMsgDBHdr> mTemplateHdr; + nsCOMPtr<nsIMsgWindow> mMsgWindow; + nsCOMPtr<nsIMsgIdentity> mIdentity; + nsCString mTemplateBody; + bool mInMsgBody; + char mLastBlockChars[3]; + + private: + ~nsMsgTemplateReplyHelper(); +}; + +NS_IMPL_ISUPPORTS(nsMsgTemplateReplyHelper, nsIStreamListener, + nsIRequestObserver, nsIUrlListener) + +nsMsgTemplateReplyHelper::nsMsgTemplateReplyHelper() { + mInMsgBody = false; + memset(mLastBlockChars, 0, sizeof(mLastBlockChars)); +} + +nsMsgTemplateReplyHelper::~nsMsgTemplateReplyHelper() {} + +NS_IMETHODIMP nsMsgTemplateReplyHelper::OnStartRunningUrl(nsIURI* aUrl) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgTemplateReplyHelper::OnStopRunningUrl(nsIURI* aUrl, + nsresult aExitCode) { + NS_ENSURE_SUCCESS(aExitCode, aExitCode); + nsresult rv; + nsCOMPtr<nsPIDOMWindowOuter> parentWindow; + if (mMsgWindow) { + nsCOMPtr<nsIDocShell> docShell; + rv = mMsgWindow->GetRootDocShell(getter_AddRefs(docShell)); + NS_ENSURE_SUCCESS(rv, rv); + parentWindow = do_GetInterface(docShell); + NS_ENSURE_TRUE(parentWindow, NS_ERROR_FAILURE); + } + + // create the compose params object + nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams( + do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv)); + if (NS_FAILED(rv) || (!pMsgComposeParams)) return rv; + nsCOMPtr<nsIMsgCompFields> compFields = + do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv); + + nsCString replyTo; + mHdrToReplyTo->GetStringProperty("replyTo", replyTo); + if (replyTo.IsEmpty()) mHdrToReplyTo->GetAuthor(getter_Copies(replyTo)); + compFields->SetTo(NS_ConvertUTF8toUTF16(replyTo)); + + nsString body; + nsString templateSubject, replySubject; + + mHdrToReplyTo->GetMime2DecodedSubject(replySubject); + mTemplateHdr->GetMime2DecodedSubject(templateSubject); + nsString subject(u"Auto: "_ns); // RFC 3834 3.1.5. + subject.Append(templateSubject); + if (!replySubject.IsEmpty()) { + subject.AppendLiteral(u" (was: "); + subject.Append(replySubject); + subject.Append(u')'); + } + + compFields->SetSubject(subject); + compFields->SetRawHeader("Auto-Submitted", "auto-replied"_ns); + + nsCString charset; + rv = mTemplateHdr->GetCharset(getter_Copies(charset)); + NS_ENSURE_SUCCESS(rv, rv); + rv = nsMsgI18NConvertToUnicode(charset, mTemplateBody, body); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "couldn't convert templ body to unicode"); + compFields->SetBody(body); + + nsCString msgUri; + nsCOMPtr<nsIMsgFolder> folder; + mHdrToReplyTo->GetFolder(getter_AddRefs(folder)); + folder->GetUriForMsg(mHdrToReplyTo, msgUri); + // populate the compose params + pMsgComposeParams->SetType(nsIMsgCompType::ReplyWithTemplate); + pMsgComposeParams->SetFormat(nsIMsgCompFormat::Default); + pMsgComposeParams->SetIdentity(mIdentity); + pMsgComposeParams->SetComposeFields(compFields); + pMsgComposeParams->SetOriginalMsgURI(msgUri); + + // create the nsIMsgCompose object to send the object + nsCOMPtr<nsIMsgCompose> pMsgCompose( + do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + /** initialize nsIMsgCompose, Send the message, wait for send completion + * response **/ + + rv = pMsgCompose->Initialize(pMsgComposeParams, parentWindow, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<mozilla::dom::Promise> promise; + return pMsgCompose->SendMsg(nsIMsgSend::nsMsgDeliverNow, mIdentity, nullptr, + nullptr, nullptr, getter_AddRefs(promise)); +} + +NS_IMETHODIMP +nsMsgTemplateReplyHelper::OnStartRequest(nsIRequest* request) { return NS_OK; } + +NS_IMETHODIMP +nsMsgTemplateReplyHelper::OnStopRequest(nsIRequest* request, nsresult status) { + if (NS_SUCCEEDED(status)) { + // now we've got the message body in mTemplateBody - + // need to set body in compose params and send the reply. + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgTemplateReplyHelper::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStream, + uint64_t srcOffset, uint32_t count) { + nsresult rv = NS_OK; + + char readBuf[1024]; + + uint64_t available; + uint32_t readCount; + uint32_t maxReadCount = sizeof(readBuf) - 1; + + rv = inStream->Available(&available); + while (NS_SUCCEEDED(rv) && available > 0) { + uint32_t bodyOffset = 0, readOffset = 0; + if (!mInMsgBody && mLastBlockChars[0]) { + memcpy(readBuf, mLastBlockChars, 3); + readOffset = 3; + maxReadCount -= 3; + } + if (maxReadCount > available) maxReadCount = (uint32_t)available; + memset(readBuf, 0, sizeof(readBuf)); + rv = inStream->Read(readBuf + readOffset, maxReadCount, &readCount); + available -= readCount; + readCount += readOffset; + // we're mainly interested in the msg body, so we need to + // find the header/body delimiter of a blank line. A blank line + // looks like <CR><CR>, <LF><LF>, or <CRLF><CRLF> + if (!mInMsgBody) { + for (uint32_t charIndex = 0; charIndex < readCount && !bodyOffset; + charIndex++) { + if (readBuf[charIndex] == '\r' || readBuf[charIndex] == '\n') { + if (charIndex + 1 < readCount) { + if (readBuf[charIndex] == readBuf[charIndex + 1]) { + // got header+body separator + bodyOffset = charIndex + 2; + break; + } else if ((charIndex + 3 < readCount) && + !strncmp(readBuf + charIndex, "\r\n\r\n", 4)) { + bodyOffset = charIndex + 4; + break; + } + } + } + } + mInMsgBody = bodyOffset != 0; + if (!mInMsgBody && readCount > 3) // still in msg hdrs + strncpy(mLastBlockChars, readBuf + readCount - 3, 3); + } + mTemplateBody.Append(readBuf + bodyOffset); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgComposeService::ReplyWithTemplate( + nsIMsgDBHdr* aMsgHdr, const nsACString& templateUri, + nsIMsgWindow* aMsgWindow, nsIMsgIncomingServer* aServer) { + // To reply with template, we need the message body of the template. + // I think we're going to need to stream the template message to ourselves, + // and construct the body, and call setBody on the compFields. + nsresult rv; + const nsPromiseFlatCString& templateUriFlat = PromiseFlatCString(templateUri); + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgAccount> account; + rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgIdentity>> identities; + rv = account->GetIdentities(identities); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString recipients; + aMsgHdr->GetRecipients(getter_Copies(recipients)); + + nsAutoCString ccList; + aMsgHdr->GetCcList(getter_Copies(ccList)); + + // Go through the identities to see to whom this was addressed. + // In case we get no match, this is likely a list/bulk/bcc/spam mail and we + // shouldn't reply. RFC 3834 2. + nsCOMPtr<nsIMsgIdentity> identity; // identity to reply from + for (auto anIdentity : identities) { + nsAutoCString identityEmail; + anIdentity->GetEmail(identityEmail); + + if (FindInReadable(identityEmail, recipients, + nsCaseInsensitiveCStringComparator) || + FindInReadable(identityEmail, ccList, + nsCaseInsensitiveCStringComparator)) { + identity = anIdentity; + break; + } + } + if (!identity) // Found no match -> don't reply. + return NS_ERROR_ABORT; + + RefPtr<nsMsgTemplateReplyHelper> helper = new nsMsgTemplateReplyHelper; + + helper->mHdrToReplyTo = aMsgHdr; + helper->mMsgWindow = aMsgWindow; + helper->mIdentity = identity; + + nsAutoCString replyTo; + aMsgHdr->GetStringProperty("replyTo", replyTo); + if (replyTo.IsEmpty()) aMsgHdr->GetAuthor(getter_Copies(replyTo)); + if (replyTo.IsEmpty()) return NS_ERROR_FAILURE; // nowhere to send the reply + + nsCOMPtr<nsIMsgFolder> templateFolder; + nsCOMPtr<nsIMsgDatabase> templateDB; + nsCString templateMsgHdrUri; + const char* query = PL_strstr(templateUriFlat.get(), "?messageId="); + if (!query) return NS_ERROR_FAILURE; + + nsAutoCString folderUri(Substring(templateUriFlat.get(), query)); + rv = GetExistingFolder(folderUri, getter_AddRefs(templateFolder)); + NS_ENSURE_SUCCESS(rv, rv); + rv = templateFolder->GetMsgDatabase(getter_AddRefs(templateDB)); + NS_ENSURE_SUCCESS(rv, rv); + + const char* subject = PL_strstr(templateUriFlat.get(), "&subject="); + if (subject) { + const char* subjectEnd = subject + strlen(subject); + nsAutoCString messageId(Substring(query + 11, subject)); + nsAutoCString subjectString(Substring(subject + 9, subjectEnd)); + templateDB->GetMsgHdrForMessageID(messageId.get(), + getter_AddRefs(helper->mTemplateHdr)); + if (helper->mTemplateHdr) + templateFolder->GetUriForMsg(helper->mTemplateHdr, templateMsgHdrUri); + // to use the subject, we'd need to expose a method to find a message by + // subject, or painfully iterate through messages...We'll try to make the + // message-id not change when saving a template first. + } + if (templateMsgHdrUri.IsEmpty()) { + // ### probably want to return a specific error and + // have the calling code disable the filter. + NS_ASSERTION(false, "failed to get msg hdr"); + return NS_ERROR_FAILURE; + } + // we need to convert the template uri, which is of the form + // <folder uri>?messageId=<messageId>&subject=<subject> + nsCOMPtr<nsIMsgMessageService> msgService; + rv = GetMessageServiceFromURI(templateMsgHdrUri, getter_AddRefs(msgService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> listenerSupports; + helper->QueryInterface(NS_GET_IID(nsISupports), + getter_AddRefs(listenerSupports)); + + nsCOMPtr<nsIURI> dummyNull; + rv = msgService->StreamMessage( + templateMsgHdrUri, listenerSupports, aMsgWindow, helper, + false, // convert data + EmptyCString(), false, getter_AddRefs(dummyNull)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> folder; + aMsgHdr->GetFolder(getter_AddRefs(folder)); + if (!folder) return NS_ERROR_NULL_POINTER; + + // We're sending a new message. Conceptually it's a reply though, so mark the + // original message as replied. + return folder->AddMessageDispositionState( + aMsgHdr, nsIMsgFolder::nsMsgDispositionState_Replied); +} + +NS_IMETHODIMP +nsMsgComposeService::ForwardMessage(const nsAString& forwardTo, + nsIMsgDBHdr* aMsgHdr, + nsIMsgWindow* aMsgWindow, + nsIMsgIncomingServer* aServer, + uint32_t aForwardType) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + + nsresult rv; + if (aForwardType == nsIMsgComposeService::kForwardAsDefault) { + int32_t forwardPref = 0; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + prefBranch->GetIntPref("mail.forward_message_mode", &forwardPref); + // 0=default as attachment 2=forward as inline with attachments, + // (obsolete 4.x value)1=forward as quoted (mapped to 2 in mozilla) + aForwardType = forwardPref == 0 ? nsIMsgComposeService::kForwardAsAttachment + : nsIMsgComposeService::kForwardInline; + } + nsCString msgUri; + + nsCOMPtr<nsIMsgFolder> folder; + aMsgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_TRUE(folder, NS_ERROR_NULL_POINTER); + + folder->GetUriForMsg(aMsgHdr, msgUri); + + nsAutoCString uriToOpen(msgUri); + + // get the MsgIdentity for the above key using AccountManager + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgAccount> account; + nsCOMPtr<nsIMsgIdentity> identity; + + rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account)); + NS_ENSURE_SUCCESS(rv, rv); + rv = account->GetDefaultIdentity(getter_AddRefs(identity)); + // Use default identity if no identity has been found on this account + if (NS_FAILED(rv) || !identity) { + rv = GetDefaultIdentity(getter_AddRefs(identity)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aForwardType == nsIMsgComposeService::kForwardInline) + return RunMessageThroughMimeDraft( + uriToOpen, nsMimeOutput::nsMimeMessageDraftOrTemplate, identity, + uriToOpen, aMsgHdr, true, forwardTo, false, aMsgWindow, false); + + nsCOMPtr<mozIDOMWindowProxy> parentWindow; + if (aMsgWindow) { + nsCOMPtr<nsIDocShell> docShell; + rv = aMsgWindow->GetRootDocShell(getter_AddRefs(docShell)); + NS_ENSURE_SUCCESS(rv, rv); + parentWindow = do_GetInterface(docShell); + NS_ENSURE_TRUE(parentWindow, NS_ERROR_FAILURE); + } + // create the compose params object + nsCOMPtr<nsIMsgComposeParams> pMsgComposeParams( + do_CreateInstance("@mozilla.org/messengercompose/composeparams;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgCompFields> compFields = + do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv); + + compFields->SetTo(forwardTo); + // populate the compose params + pMsgComposeParams->SetType(nsIMsgCompType::ForwardAsAttachment); + pMsgComposeParams->SetFormat(nsIMsgCompFormat::Default); + pMsgComposeParams->SetIdentity(identity); + pMsgComposeParams->SetComposeFields(compFields); + pMsgComposeParams->SetOriginalMsgURI(uriToOpen); + // create the nsIMsgCompose object to send the object + nsCOMPtr<nsIMsgCompose> pMsgCompose( + do_CreateInstance("@mozilla.org/messengercompose/compose;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + /** initialize nsIMsgCompose, Send the message, wait for send completion + * response **/ + rv = pMsgCompose->Initialize(pMsgComposeParams, parentWindow, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Promise> promise; + rv = pMsgCompose->SendMsg(nsIMsgSend::nsMsgDeliverNow, identity, nullptr, + nullptr, nullptr, getter_AddRefs(promise)); + NS_ENSURE_SUCCESS(rv, rv); + + // nsMsgCompose::ProcessReplyFlags usually takes care of marking messages + // as forwarded. ProcessReplyFlags is normally called from + // nsMsgComposeSendListener::OnStopSending but for this case the msgCompose + // object is not set so ProcessReplyFlags won't get called. + // Therefore, let's just mark it here instead. + return folder->AddMessageDispositionState( + aMsgHdr, nsIMsgFolder::nsMsgDispositionState_Forwarded); +} + +nsresult nsMsgComposeService::AddGlobalHtmlDomains() { + nsresult rv; + nsCOMPtr<nsIPrefService> prefs = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch; + rv = prefs->GetBranch(MAILNEWS_ROOT_PREF, getter_AddRefs(prefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> defaultsPrefBranch; + rv = prefs->GetDefaultBranch(MAILNEWS_ROOT_PREF, + getter_AddRefs(defaultsPrefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + /** + * Check to see if we need to add any global domains. + * If so, make sure the following prefs are added to mailnews.js + * + * 1. pref("mailnews.global_html_domains.version", version number); + * This pref registers the current version in the user prefs file. A default + * value is stored in mailnews file. Depending the changes we plan to make we + * can move the default version number. Comparing version number from user's + * prefs file and the default one from mailnews.js, we can effect ppropriate + * changes. + * + * 2. pref("mailnews.global_html_domains", <comma separated domain list>); + * This pref contains the list of html domains that ISP can add to make that + * user's contain all of these under the HTML domains in the + * Mail&NewsGrpus|Send Format under global preferences. + */ + int32_t htmlDomainListCurrentVersion, htmlDomainListDefaultVersion; + rv = prefBranch->GetIntPref(HTMLDOMAINUPDATE_VERSION_PREF_NAME, + &htmlDomainListCurrentVersion); + NS_ENSURE_SUCCESS(rv, rv); + + rv = defaultsPrefBranch->GetIntPref(HTMLDOMAINUPDATE_VERSION_PREF_NAME, + &htmlDomainListDefaultVersion); + NS_ENSURE_SUCCESS(rv, rv); + + // Update the list as needed + if (htmlDomainListCurrentVersion <= htmlDomainListDefaultVersion) { + // Get list of global domains need to be added + nsCString globalHtmlDomainList; + rv = prefBranch->GetCharPref(HTMLDOMAINUPDATE_DOMAINLIST_PREF_NAME, + globalHtmlDomainList); + + if (NS_SUCCEEDED(rv) && !globalHtmlDomainList.IsEmpty()) { + nsTArray<nsCString> domainArray; + + // Get user's current HTML domain set for send format + nsCString currentHtmlDomainList; + rv = prefBranch->GetCharPref(USER_CURRENT_HTMLDOMAINLIST_PREF_NAME, + currentHtmlDomainList); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString newHtmlDomainList(currentHtmlDomainList); + // Get the current html domain list into new list var + ParseString(currentHtmlDomainList, DOMAIN_DELIMITER, domainArray); + + // Get user's current Plaintext domain set for send format + nsCString currentPlaintextDomainList; + rv = prefBranch->GetCharPref(USER_CURRENT_PLAINTEXTDOMAINLIST_PREF_NAME, + currentPlaintextDomainList); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the current plaintext domain list into new list var + ParseString(currentPlaintextDomainList, DOMAIN_DELIMITER, domainArray); + + size_t i = domainArray.Length(); + if (i > 0) { + // Append each domain in the preconfigured html domain list + globalHtmlDomainList.StripWhitespace(); + ParseString(globalHtmlDomainList, DOMAIN_DELIMITER, domainArray); + + // Now add each domain that does not already appear in + // the user's current html or plaintext domain lists + for (; i < domainArray.Length(); i++) { + if (domainArray.IndexOf(domainArray[i]) == i) { + if (!newHtmlDomainList.IsEmpty()) + newHtmlDomainList += DOMAIN_DELIMITER; + newHtmlDomainList += domainArray[i]; + } + } + } else { + // User has no domains listed either in html or plain text category. + // Assign the global list to be the user's current html domain list + newHtmlDomainList = globalHtmlDomainList; + } + + // Set user's html domain pref with the updated list + rv = prefBranch->SetCharPref(USER_CURRENT_HTMLDOMAINLIST_PREF_NAME, + newHtmlDomainList); + NS_ENSURE_SUCCESS(rv, rv); + + // Increase the version to avoid running the update code unless needed + // (based on default version) + rv = prefBranch->SetIntPref(HTMLDOMAINUPDATE_VERSION_PREF_NAME, + htmlDomainListCurrentVersion + 1); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgComposeService::RegisterComposeDocShell(nsIDocShell* aDocShell, + nsIMsgCompose* aComposeObject) { + NS_ENSURE_ARG_POINTER(aDocShell); + NS_ENSURE_ARG_POINTER(aComposeObject); + + nsresult rv; + + // add the msg compose / dom window mapping to our hash table + nsWeakPtr weakDocShell = do_GetWeakReference(aDocShell, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsWeakPtr weakMsgComposePtr = do_GetWeakReference(aComposeObject); + NS_ENSURE_SUCCESS(rv, rv); + mOpenComposeWindows.InsertOrUpdate(weakDocShell, weakMsgComposePtr); + + return rv; +} + +NS_IMETHODIMP +nsMsgComposeService::UnregisterComposeDocShell(nsIDocShell* aDocShell) { + NS_ENSURE_ARG_POINTER(aDocShell); + + nsresult rv; + nsWeakPtr weakDocShell = do_GetWeakReference(aDocShell, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mOpenComposeWindows.Remove(weakDocShell); + + return rv; +} + +NS_IMETHODIMP +nsMsgComposeService::GetMsgComposeForDocShell(nsIDocShell* aDocShell, + nsIMsgCompose** aComposeObject) { + NS_ENSURE_ARG_POINTER(aDocShell); + NS_ENSURE_ARG_POINTER(aComposeObject); + + if (!mOpenComposeWindows.Count()) return NS_ERROR_FAILURE; + + // get the weak reference for our dom window + nsresult rv; + nsWeakPtr weakDocShell = do_GetWeakReference(aDocShell, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsWeakPtr weakMsgComposePtr; + + if (!mOpenComposeWindows.Get(weakDocShell, getter_AddRefs(weakMsgComposePtr))) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMsgCompose> msgCompose = do_QueryReferent(weakMsgComposePtr, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*aComposeObject = msgCompose); + return rv; +} + +/** + * LoadDraftOrTemplate + * Helper routine used to run msgURI through libmime in order to fetch the + * contents for a draft or template. + */ +nsresult nsMsgComposeService::LoadDraftOrTemplate( + const nsACString& aMsgURI, nsMimeOutputType aOutType, + nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI, + nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, bool overrideComposeFormat, + nsIMsgWindow* aMsgWindow, bool autodetectCharset) { + return RunMessageThroughMimeDraft( + aMsgURI, aOutType, aIdentity, aOriginalMsgURI, aOrigMsgHdr, + aForwardInline, EmptyString(), overrideComposeFormat, aMsgWindow, + autodetectCharset); +} + +/** + * Run the aMsgURI message through libmime. We set various attributes of the + * nsIMimeStreamConverter so mimedrft.cpp will know what to do with the message + * when its done streaming. Usually that will be opening a compose window + * with the contents of the message, but if forwardTo is non-empty, mimedrft.cpp + * will forward the contents directly. + * + * @param aMsgURI URI to stream, which is the msgUri + any extra terms, e.g., + * "redirect=true". + * @param aOutType nsMimeOutput::nsMimeMessageDraftOrTemplate or + * nsMimeOutput::nsMimeMessageEditorTemplate + * @param aIdentity identity to use for the new message + * @param aOriginalMsgURI msgURI w/o any extra terms + * @param aOrigMsgHdr nsIMsgDBHdr corresponding to aOriginalMsgURI + * @param aForwardInline true if doing a forward inline + * @param aForwardTo e-mail address to forward msg to. This is used for + * forward inline message filter actions. + * @param aOverrideComposeFormat True if the user had shift key down when + doing a command that opens the compose window, + * which means we switch the compose window used + * from the default. + * @param aMsgWindow msgWindow to pass into LoadMessage. + */ +nsresult nsMsgComposeService::RunMessageThroughMimeDraft( + const nsACString& aMsgURI, nsMimeOutputType aOutType, + nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI, + nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, const nsAString& aForwardTo, + bool aOverrideComposeFormat, nsIMsgWindow* aMsgWindow, + bool autodetectCharset) { + nsCOMPtr<nsIMsgMessageService> messageService; + nsresult rv = + GetMessageServiceFromURI(aMsgURI, getter_AddRefs(messageService)); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a mime parser (nsIMimeStreamConverter)to do the conversion. + nsCOMPtr<nsIMimeStreamConverter> mimeConverter = do_CreateInstance( + "@mozilla.org/streamconv;1?from=message/rfc822&to=application/xhtml+xml", + &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mimeConverter->SetMimeOutputType( + aOutType); // Set the type of output for libmime + mimeConverter->SetForwardInline(aForwardInline); + if (!aForwardTo.IsEmpty()) { + mimeConverter->SetForwardInlineFilter(true); + mimeConverter->SetForwardToAddress(aForwardTo); + } + mimeConverter->SetOverrideComposeFormat(aOverrideComposeFormat); + mimeConverter->SetIdentity(aIdentity); + mimeConverter->SetOriginalMsgURI(aOriginalMsgURI); + mimeConverter->SetOrigMsgHdr(aOrigMsgHdr); + + nsCOMPtr<nsIURI> url; + bool fileUrl = StringBeginsWith(aMsgURI, "file:"_ns); + nsCString mailboxUri(aMsgURI); + if (fileUrl) { + // We loaded a .eml file from a file: url. Construct equivalent mailbox url. + mailboxUri.Replace(0, 5, "mailbox:"_ns); + mailboxUri.AppendLiteral("&number=0"); + // Need this to prevent nsMsgCompose::TagEmbeddedObjects from setting + // inline images as moz-do-not-send. + mimeConverter->SetOriginalMsgURI(mailboxUri); + } + if (fileUrl || PromiseFlatCString(aMsgURI).Find( + "&type=application/x-message-display") >= 0) + rv = NS_NewURI(getter_AddRefs(url), mailboxUri); + else + rv = messageService->GetUrlForUri(aMsgURI, aMsgWindow, getter_AddRefs(url)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(url); + if (!mailnewsurl) { + NS_WARNING( + "Trying to run a message through MIME which doesn't have a " + "nsIMsgMailNewsUrl?"); + return NS_ERROR_UNEXPECTED; + } + // SetSpecInternal must not fail, or else the URL won't have a base URL and + // we'll crash later. + rv = mailnewsurl->SetSpecInternal(mailboxUri); + NS_ENSURE_SUCCESS(rv, rv); + + // if we are forwarding a message and that message used a charset override + // then forward that as auto-detect flag, too. + nsCOMPtr<nsIMsgI18NUrl> i18nUrl(do_QueryInterface(url)); + if (i18nUrl) (void)i18nUrl->SetAutodetectCharset(autodetectCharset); + + nsCOMPtr<nsIPrincipal> nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewInputStreamChannel( + getter_AddRefs(channel), url, nullptr, nullPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewChannel failed."); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIStreamConverter> converter = do_QueryInterface(mimeConverter); + rv = converter->AsyncConvertData(nullptr, nullptr, nullptr, channel); + NS_ENSURE_SUCCESS(rv, rv); + + // Now, just plug the two together and get the hell out of the way! + nsCOMPtr<nsIStreamListener> streamListener = do_QueryInterface(mimeConverter); + return messageService->LoadMessage(aMsgURI, streamListener, aMsgWindow, + nullptr, autodetectCharset); +} + +NS_IMETHODIMP +nsMsgComposeService::Handle(nsICommandLine* aCmdLine) { + NS_ENSURE_ARG_POINTER(aCmdLine); + + nsresult rv; + int32_t found, end, count; + nsAutoString uristr; + bool composeShouldHandle = true; + + rv = aCmdLine->FindFlag(u"compose"_ns, false, &found); + NS_ENSURE_SUCCESS(rv, rv); + +#ifndef MOZ_SUITE + // MAC OS X passes in -url mailto:mscott@mozilla.org into the command line + // instead of -compose. + if (found == -1) { + rv = aCmdLine->FindFlag(u"url"_ns, false, &found); + NS_ENSURE_SUCCESS(rv, rv); + // we don't want to consume the argument for -url unless we're sure it is a + // mailto url and we'll figure that out shortly. + composeShouldHandle = false; + } +#endif + + if (found == -1) return NS_OK; + + end = found; + + rv = aCmdLine->GetLength(&count); + NS_ENSURE_SUCCESS(rv, rv); + + if (count > found + 1) { + aCmdLine->GetArgument(found + 1, uristr); + if (StringBeginsWith(uristr, u"mailto:"_ns) || + StringBeginsWith(uristr, u"preselectid="_ns) || + StringBeginsWith(uristr, u"to="_ns) || + StringBeginsWith(uristr, u"cc="_ns) || + StringBeginsWith(uristr, u"bcc="_ns) || + StringBeginsWith(uristr, u"newsgroups="_ns) || + StringBeginsWith(uristr, u"subject="_ns) || + StringBeginsWith(uristr, u"format="_ns) || + StringBeginsWith(uristr, u"body="_ns) || + StringBeginsWith(uristr, u"attachment="_ns) || + StringBeginsWith(uristr, u"message="_ns) || + StringBeginsWith(uristr, u"from="_ns)) { + composeShouldHandle = true; // the -url argument looks like mailto + end++; + // mailto: URIs are frequently passed with spaces in them. They should be + // escaped with %20, but we hack around broken clients. See bug 231032. + while (end + 1 < count) { + nsAutoString curarg; + aCmdLine->GetArgument(end + 1, curarg); + if (curarg.First() == '-') break; + + uristr.Append(' '); + uristr.Append(curarg); + ++end; + } + } else { + uristr.Truncate(); + } + } + if (composeShouldHandle) { + aCmdLine->RemoveArguments(found, end); + + nsCOMPtr<nsIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + NS_ENSURE_TRUE(wwatch, NS_ERROR_FAILURE); + + nsCOMPtr<nsISupportsString> arg( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + if (arg) arg->SetData(uristr); + + nsCOMPtr<nsIMutableArray> params(do_CreateInstance(NS_ARRAY_CONTRACTID)); + params->AppendElement(arg); + params->AppendElement(aCmdLine); + + nsCOMPtr<mozIDOMWindowProxy> opened; + wwatch->OpenWindow(nullptr, DEFAULT_CHROME, "_blank"_ns, + "chrome,dialog=no,all"_ns, params, + getter_AddRefs(opened)); + + aCmdLine->SetPreventDefault(true); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgComposeService::GetHelpInfo(nsACString& aResult) { + // clang-format off + aResult.AssignLiteral( + " -compose [ <options> ] Compose a mail or news message. Options are specified\n" + " as string \"option='value,...',option=value,...\" and\n" + " include: from, to, cc, bcc, newsgroups, subject, body,\n" + " message (file), attachment (file), format (html | text).\n" + " Example: \"to=john@example.com,subject='Dinner tonight?'\"\n"); + return NS_OK; + // clang-format on +} diff --git a/comm/mailnews/compose/src/nsMsgComposeService.h b/comm/mailnews/compose/src/nsMsgComposeService.h new file mode 100644 index 0000000000..97911fb71f --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgComposeService.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#define MSGCOMP_TRACE_PERFORMANCE 1 + +#include "nsIMsgComposeService.h" +#include "nsCOMPtr.h" +#include "mozIDOMWindow.h" +#include "nsIAppWindow.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsIWeakReference.h" +#include "nsIMimeStreamConverter.h" +#include "nsInterfaceHashtable.h" + +#include "nsICommandLineHandler.h" +#define ICOMMANDLINEHANDLER nsICommandLineHandler + +class nsMsgComposeService : public nsIMsgComposeService, + public ICOMMANDLINEHANDLER, + public nsSupportsWeakReference { + public: + nsMsgComposeService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGCOMPOSESERVICE + NS_DECL_NSICOMMANDLINEHANDLER + + nsresult Init(); + void Reset(); + void DeleteCachedWindows(); + nsresult AddGlobalHtmlDomains(); + + private: + virtual ~nsMsgComposeService(); + bool mLogComposePerformance; + + nsresult LoadDraftOrTemplate( + const nsACString& aMsgURI, nsMimeOutputType aOutType, + nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI, + nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, bool overrideComposeFormat, + nsIMsgWindow* aMsgWindow, bool autodetectCharset); + + nsresult RunMessageThroughMimeDraft( + const nsACString& aMsgURI, nsMimeOutputType aOutType, + nsIMsgIdentity* aIdentity, const nsACString& aOriginalMsgURI, + nsIMsgDBHdr* aOrigMsgHdr, bool aForwardInline, const nsAString& forwardTo, + bool overrideComposeFormat, nsIMsgWindow* aMsgWindow, + bool autodetectCharset); + + // hash table mapping dom windows to nsIMsgCompose objects + nsInterfaceHashtable<nsISupportsHashKey, nsIWeakReference> + mOpenComposeWindows; + + // When doing a reply and the settings are enabled, get the HTML of the + // selected text in the original message window so that it can be quoted + // instead of the entire message. + nsresult GetOrigWindowSelection(MSG_ComposeType type, + mozilla::dom::Selection* selection, + nsACString& aSelHTML); + +#ifdef MSGCOMP_TRACE_PERFORMANCE + PRIntervalTime mStartTime; + PRIntervalTime mPreviousTime; +#endif +}; diff --git a/comm/mailnews/compose/src/nsMsgCopy.cpp b/comm/mailnews/compose/src/nsMsgCopy.cpp new file mode 100644 index 0000000000..0cccc108f9 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCopy.cpp @@ -0,0 +1,471 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsMsgCopy.h" + +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsIThread.h" +#include "nscore.h" +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/RefCountType.h" +#include "mozilla/RefPtr.h" +#include "nsMsgFolderFlags.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFolder.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgFolder.h" +#include "nsIMsgIncomingServer.h" +#include "nsIMsgProtocolInfo.h" +#include "nsISupports.h" +#include "nsIURL.h" +#include "nsNetCID.h" +#include "nsMsgCompUtils.h" +#include "prcmon.h" +#include "nsIMsgImapMailFolder.h" +#include "nsThreadUtils.h" +#include "nsIMsgWindow.h" +#include "nsIMsgProgress.h" +#include "nsComposeStrings.h" +#include "prmem.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsMsgUtils.h" +#include "nsIURIMutator.h" + +//////////////////////////////////////////////////////////////////////////////////// +// This is the listener class for the copy operation. We have to create this +// class to listen for message copy completion and eventually notify the caller +//////////////////////////////////////////////////////////////////////////////////// +NS_IMPL_ISUPPORTS(CopyListener, nsIMsgCopyServiceListener) + +CopyListener::CopyListener(void) { mCopyInProgress = false; } + +CopyListener::~CopyListener(void) {} + +nsresult CopyListener::OnStartCopy() { +#ifdef NS_DEBUG + printf("CopyListener::OnStartCopy()\n"); +#endif + + if (mComposeAndSend) mComposeAndSend->NotifyListenerOnStartCopy(); + return NS_OK; +} + +nsresult CopyListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { +#ifdef NS_DEBUG + printf("CopyListener::OnProgress() %d of %d\n", aProgress, aProgressMax); +#endif + + if (mComposeAndSend) + mComposeAndSend->NotifyListenerOnProgressCopy(aProgress, aProgressMax); + + return NS_OK; +} + +nsresult CopyListener::SetMessageKey(nsMsgKey aMessageKey) { + if (mComposeAndSend) mComposeAndSend->SetMessageKey(aMessageKey); + return NS_OK; +} + +NS_IMETHODIMP +CopyListener::GetMessageId(nsACString& aMessageId) { + if (mComposeAndSend) mComposeAndSend->GetMessageId(aMessageId); + return NS_OK; +} + +nsresult CopyListener::OnStopCopy(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { +#ifdef NS_DEBUG + printf("CopyListener: SUCCESSFUL ON THE COPY OPERATION!\n"); +#endif + } else { +#ifdef NS_DEBUG + printf("CopyListener: COPY OPERATION FAILED!\n"); +#endif + } + + if (mCopyInProgress) { + PR_CEnterMonitor(this); + PR_CNotifyAll(this); + mCopyInProgress = false; + PR_CExitMonitor(this); + } + if (mComposeAndSend) mComposeAndSend->NotifyListenerOnStopCopy(aStatus); + + return NS_OK; +} + +nsresult CopyListener::SetMsgComposeAndSendObject(nsIMsgSend* obj) { + if (obj) mComposeAndSend = obj; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////////// +// END END END END END END END END END END END END END END END +// This is the listener class for the copy operation. We have to create this +// class to listen for message copy completion and eventually notify the caller +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsMsgCopy, nsIMsgCopy, nsIUrlListener) + +nsMsgCopy::nsMsgCopy() { + mFile = nullptr; + mMode = nsIMsgSend::nsMsgDeliverNow; + mSavePref = nullptr; + mIsDraft = false; + mMsgFlags = nsMsgMessageFlags::Read; +} + +nsMsgCopy::~nsMsgCopy() { PR_Free(mSavePref); } + +NS_IMETHODIMP +nsMsgCopy::StartCopyOperation(nsIMsgIdentity* aUserIdentity, nsIFile* aFile, + nsMsgDeliverMode aMode, nsIMsgSend* aMsgSendObj, + const nsACString& aSavePref, + nsIMsgDBHdr* aMsgToReplace) { + nsCOMPtr<nsIMsgFolder> dstFolder; + bool isDraft = false; + uint32_t msgFlags = nsMsgMessageFlags::Read; + bool waitForUrl = false; + nsresult rv; + + if (!aMsgSendObj) return NS_ERROR_INVALID_ARG; + + // Store away the server location... + if (!aSavePref.IsEmpty()) mSavePref = ToNewCString(aSavePref); + + // + // Vars for implementation... + // + + // QueueForLater (Outbox) + if (aMode == nsIMsgSend::nsMsgQueueForLater || + aMode == nsIMsgSend::nsMsgDeliverBackground) { + rv = GetUnsentMessagesFolder(aUserIdentity, getter_AddRefs(dstFolder), + &waitForUrl); + isDraft = false; + // Do not mark outgoing messages as read. + msgFlags = 0; + if (!dstFolder || NS_FAILED(rv)) { + return NS_MSG_UNABLE_TO_SEND_LATER; + } + } else if (aMode == nsIMsgSend::nsMsgSaveAsDraft) // SaveAsDraft (Drafts) + { + rv = GetDraftsFolder(aUserIdentity, getter_AddRefs(dstFolder), &waitForUrl); + isDraft = true; + // Do not mark drafts as read. + msgFlags = 0; + if (!dstFolder || NS_FAILED(rv)) return NS_MSG_UNABLE_TO_SAVE_DRAFT; + } else if (aMode == + nsIMsgSend::nsMsgSaveAsTemplate) // SaveAsTemplate (Templates) + { + rv = GetTemplatesFolder(aUserIdentity, getter_AddRefs(dstFolder), + &waitForUrl); + // Mark saved templates as read. + isDraft = false; + msgFlags = nsMsgMessageFlags::Read; + if (!dstFolder || NS_FAILED(rv)) return NS_MSG_UNABLE_TO_SAVE_TEMPLATE; + } else // SaveInSentFolder (Sent) - nsMsgDeliverNow or nsMsgSendUnsent + { + rv = GetSentFolder(aUserIdentity, getter_AddRefs(dstFolder), &waitForUrl); + // Mark send messages as read. + isDraft = false; + msgFlags = nsMsgMessageFlags::Read; + if (!dstFolder || NS_FAILED(rv)) return NS_MSG_COULDNT_OPEN_FCC_FOLDER; + } + + nsCOMPtr<nsIMsgWindow> msgWindow; + + if (aMsgSendObj) { + nsCOMPtr<nsIMsgProgress> progress; + aMsgSendObj->GetProgress(getter_AddRefs(progress)); + if (progress) progress->GetMsgWindow(getter_AddRefs(msgWindow)); + } + + mMode = aMode; + mFile = aFile; + mDstFolder = dstFolder; + mMsgToReplace = aMsgToReplace; + mIsDraft = isDraft; + mMsgSendObj = aMsgSendObj; + mMsgFlags = msgFlags; + if (!waitForUrl) { + // cache info needed for DoCopy and call DoCopy when OnStopUrl is called. + rv = DoCopy(aFile, dstFolder, aMsgToReplace, isDraft, msgFlags, msgWindow, + aMsgSendObj); + // N.B. "this" may be deleted when this call returns. + } + return rv; +} + +nsresult nsMsgCopy::DoCopy(nsIFile* aDiskFile, nsIMsgFolder* dstFolder, + nsIMsgDBHdr* aMsgToReplace, bool aIsDraft, + uint32_t aMsgFlags, nsIMsgWindow* msgWindow, + nsIMsgSend* aMsgSendObj) { + nsresult rv = NS_OK; + + // Check sanity + if ((!aDiskFile) || (!dstFolder)) return NS_ERROR_INVALID_ARG; + + // Call copyservice with dstFolder, disk file, and txnManager + if (NS_SUCCEEDED(rv)) { + RefPtr<CopyListener> copyListener = new CopyListener(); + if (!copyListener) return NS_ERROR_OUT_OF_MEMORY; + + copyListener->SetMsgComposeAndSendObject(aMsgSendObj); + nsCOMPtr<nsIThread> thread; + + if (aIsDraft) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(dstFolder); + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_FAILED(rv)) return rv; + bool shutdownInProgress = false; + rv = accountManager->GetShutdownInProgress(&shutdownInProgress); + + if (NS_SUCCEEDED(rv) && shutdownInProgress && imapFolder) { + // set the following only when we were in the middle of shutdown + // process + copyListener->mCopyInProgress = true; + thread = do_GetCurrentThread(); + } + } + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = copyService->CopyFileMessage(aDiskFile, dstFolder, aMsgToReplace, + aIsDraft, aMsgFlags, EmptyCString(), + copyListener, msgWindow); + // copyListener->mCopyInProgress can only be set when we are in the + // middle of the shutdown process + while (copyListener->mCopyInProgress) { + PR_CEnterMonitor(copyListener); + PR_CWait(copyListener, PR_MicrosecondsToInterval(1000UL)); + PR_CExitMonitor(copyListener); + if (thread) NS_ProcessPendingEvents(thread); + } + } + + return rv; +} + +NS_IMETHODIMP +nsMsgCopy::GetDstFolder(nsIMsgFolder** aDstFolder) { + NS_ENSURE_ARG_POINTER(aDstFolder); + NS_IF_ADDREF(*aDstFolder = mDstFolder); + return NS_OK; +} + +// nsIUrlListener methods +NS_IMETHODIMP +nsMsgCopy::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; } + +NS_IMETHODIMP +nsMsgCopy::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + nsresult rv = aExitCode; + if (NS_SUCCEEDED(aExitCode)) { + rv = DoCopy(mFile, mDstFolder, mMsgToReplace, mIsDraft, mMsgFlags, nullptr, + mMsgSendObj); + } + return rv; +} + +nsresult nsMsgCopy::GetUnsentMessagesFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** folder, + bool* waitForUrl) { + nsresult ret = LocateMessageFolder( + userIdentity, nsIMsgSend::nsMsgQueueForLater, mSavePref, folder); + if (*folder) (*folder)->SetFlag(nsMsgFolderFlags::Queue); + CreateIfMissing(folder, waitForUrl); + return ret; +} + +nsresult nsMsgCopy::GetDraftsFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** folder, bool* waitForUrl) { + nsresult ret = LocateMessageFolder(userIdentity, nsIMsgSend::nsMsgSaveAsDraft, + mSavePref, folder); + if (*folder) (*folder)->SetFlag(nsMsgFolderFlags::Drafts); + CreateIfMissing(folder, waitForUrl); + return ret; +} + +nsresult nsMsgCopy::GetTemplatesFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** folder, + bool* waitForUrl) { + nsresult ret = LocateMessageFolder( + userIdentity, nsIMsgSend::nsMsgSaveAsTemplate, mSavePref, folder); + if (*folder) (*folder)->SetFlag(nsMsgFolderFlags::Templates); + CreateIfMissing(folder, waitForUrl); + return ret; +} + +nsresult nsMsgCopy::GetSentFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** folder, bool* waitForUrl) { + nsresult ret = LocateMessageFolder(userIdentity, nsIMsgSend::nsMsgDeliverNow, + mSavePref, folder); + if (*folder) { + // If mSavePref is the same as the identity's fcc folder, set the sent flag. + nsCString identityFccUri; + userIdentity->GetFccFolder(identityFccUri); + if (identityFccUri.Equals(mSavePref)) + (*folder)->SetFlag(nsMsgFolderFlags::SentMail); + } + CreateIfMissing(folder, waitForUrl); + return ret; +} + +nsresult nsMsgCopy::CreateIfMissing(nsIMsgFolder** folder, bool* waitForUrl) { + nsresult rv = NS_OK; + if (folder && *folder) { + nsCOMPtr<nsIMsgFolder> parent; + (*folder)->GetParent(getter_AddRefs(parent)); + if (!parent) { + nsCOMPtr<nsIFile> folderPath; + // for local folders, path is to the berkeley mailbox. + // for imap folders, path needs to have .msf appended to the name + (*folder)->GetFilePath(getter_AddRefs(folderPath)); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = (*folder)->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = server->GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isAsyncFolder; + rv = protocolInfo->GetFoldersCreatedAsync(&isAsyncFolder); + NS_ENSURE_SUCCESS(rv, rv); + + // if we can't get the path from the folder, then try to create the + // storage. for imap, it doesn't matter if the .msf file exists - it still + // might not exist on the server, so we should try to create it + bool exists = false; + if (!isAsyncFolder && folderPath) folderPath->Exists(&exists); + if (!exists) { + (*folder)->CreateStorageIfMissing(this); + if (isAsyncFolder) *waitForUrl = true; + + rv = NS_OK; + } + } + } + return rv; +} +//////////////////////////////////////////////////////////////////////////////////// +// Utility Functions for MsgFolders +//////////////////////////////////////////////////////////////////////////////////// +nsresult LocateMessageFolder(nsIMsgIdentity* userIdentity, + nsMsgDeliverMode aFolderType, + const char* aFolderURI, nsIMsgFolder** msgFolder) { + nsresult rv = NS_OK; + + if (!msgFolder) return NS_ERROR_NULL_POINTER; + *msgFolder = nullptr; + + if (!aFolderURI || !*aFolderURI) return NS_ERROR_INVALID_ARG; + + // as long as it doesn't start with anyfolder:// + if (PL_strncasecmp(ANY_SERVER, aFolderURI, strlen(aFolderURI)) != 0) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(nsDependentCString(aFolderURI), + getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + // Don't check validity of folder - caller will handle creating it. + nsCOMPtr<nsIMsgIncomingServer> server; + // make sure that folder hierarchy is built so that legitimate parent-child + // relationship is established. + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetMsgFolderFromURI(folder, nsDependentCString(aFolderURI), + msgFolder); + } else { + if (!userIdentity) return NS_ERROR_INVALID_ARG; + + // get the account manager + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // If any folder will do, go look for one. + nsTArray<RefPtr<nsIMsgIncomingServer>> servers; + rv = accountManager->GetServersForIdentity(userIdentity, servers); + NS_ENSURE_SUCCESS(rv, rv); + + // Ok, we have to look through the servers and try to find the server that + // has a valid folder of the type that interests us... + for (auto inServer : servers) { + // Now that we have the server...we need to get the named message folder + + // If aFolderURI is passed in, then the user has chosen a specific + // mail folder to save the message, but if it is null, just find the + // first one and make that work. The folder is specified as a URI, like + // the following: + // + // mailbox://nobody@Local Folders/Sent + // imap://rhp@nsmail-2/Drafts + // newsgroup://news.mozilla.org/netscape.test + // + nsCString serverURI; + rv = inServer->GetServerURI(serverURI); + if (NS_FAILED(rv) || serverURI.IsEmpty()) continue; + + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = inServer->GetRootFolder(getter_AddRefs(rootFolder)); + + if (NS_FAILED(rv) || (!rootFolder)) continue; + + // use the defaults by getting the folder by flags + if (aFolderType == nsIMsgSend::nsMsgQueueForLater || + aFolderType == nsIMsgSend::nsMsgDeliverBackground) { + // QueueForLater (Outbox) + rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Queue, msgFolder); + } else if (aFolderType == + nsIMsgSend::nsMsgSaveAsDraft) // SaveAsDraft (Drafts) + { + rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Drafts, msgFolder); + } else if (aFolderType == + nsIMsgSend::nsMsgSaveAsTemplate) // SaveAsTemplate (Templates) + { + rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Templates, msgFolder); + } else // SaveInSentFolder (Sent) - nsMsgDeliverNow or nsMsgSendUnsent + { + rootFolder->GetFolderWithFlags(nsMsgFolderFlags::SentMail, msgFolder); + } + + if (*msgFolder) { + return NS_OK; + } + } + } + return NS_ERROR_FAILURE; +} + +// +// Figure out if a folder is local or not and return a boolean to +// say so. +// +nsresult MessageFolderIsLocal(nsIMsgIdentity* userIdentity, + nsMsgDeliverMode aFolderType, + const char* aFolderURI, bool* aResult) { + nsresult rv; + + if (!aFolderURI) return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsIURL> url; + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(nsDependentCString(aFolderURI)) + .Finalize(url); + if (NS_FAILED(rv)) return rv; + + /* mailbox:/ means its local (on disk) */ + rv = url->SchemeIs("mailbox", aResult); + if (NS_FAILED(rv)) return rv; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgCopy.h b/comm/mailnews/compose/src/nsMsgCopy.h new file mode 100644 index 0000000000..4fa50bd8ac --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgCopy.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgCopy_H_ +#define _nsMsgCopy_H_ + +#include "mozilla/Attributes.h" +#include "nscore.h" +#include "nsIFile.h" +#include "nsIMsgHdr.h" +#include "nsIMsgFolder.h" +#include "nsITransactionManager.h" +#include "nsIMsgCopy.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIMsgCopyService.h" + +// Forward declarations... +class nsMsgCopy; + +//////////////////////////////////////////////////////////////////////////////////// +// This is the listener class for the copy operation. We have to create this +// class to listen for message copy completion and eventually notify the caller +//////////////////////////////////////////////////////////////////////////////////// +class CopyListener : public nsIMsgCopyServiceListener { + public: + CopyListener(void); + + // nsISupports interface + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD OnStartCopy() override; + + NS_IMETHOD OnProgress(uint32_t aProgress, uint32_t aProgressMax) override; + + NS_IMETHOD SetMessageKey(nsMsgKey aMessageKey) override; + + NS_IMETHOD GetMessageId(nsACString& aMessageId) override; + + NS_IMETHOD OnStopCopy(nsresult aStatus) override; + + NS_IMETHOD SetMsgComposeAndSendObject(nsIMsgSend* obj); + + bool mCopyInProgress; + + private: + virtual ~CopyListener(); + nsCOMPtr<nsIMsgSend> mComposeAndSend; +}; + +// +// This is a class that deals with processing remote attachments. It implements +// an nsIStreamListener interface to deal with incoming data +// +class nsMsgCopy : public nsIMsgCopy, public nsIUrlListener { + public: + nsMsgCopy(); + + // nsISupports interface + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGCOPY + NS_DECL_NSIURLLISTENER + + ////////////////////////////////////////////////////////////////////// + // Object methods... + ////////////////////////////////////////////////////////////////////// + // + nsresult DoCopy(nsIFile* aDiskFile, nsIMsgFolder* dstFolder, + nsIMsgDBHdr* aMsgToReplace, bool aIsDraft, uint32_t aMsgFlags, + nsIMsgWindow* msgWindow, nsIMsgSend* aMsgSendObj); + + nsresult GetUnsentMessagesFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** msgFolder, bool* waitForUrl); + nsresult GetDraftsFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** msgFolder, bool* waitForUrl); + nsresult GetTemplatesFolder(nsIMsgIdentity* userIdentity, + nsIMsgFolder** msgFolder, bool* waitForUrl); + nsresult GetSentFolder(nsIMsgIdentity* userIdentity, nsIMsgFolder** msgFolder, + bool* waitForUrl); + nsresult CreateIfMissing(nsIMsgFolder** folder, bool* waitForUrl); + + // + // Vars for implementation... + // + nsIFile* mFile; // the file we are sending... + nsMsgDeliverMode mMode; + nsCOMPtr<nsIMsgFolder> mDstFolder; + nsCOMPtr<nsIMsgDBHdr> mMsgToReplace; + bool mIsDraft; + uint32_t mMsgFlags; + nsCOMPtr<nsIMsgSend> mMsgSendObj; + char* mSavePref; + + private: + virtual ~nsMsgCopy(); +}; + +// Useful function for the back end... +nsresult LocateMessageFolder(nsIMsgIdentity* userIdentity, + nsMsgDeliverMode aFolderType, const char* aSaveURI, + nsIMsgFolder** msgFolder); + +nsresult MessageFolderIsLocal(nsIMsgIdentity* userIdentity, + nsMsgDeliverMode aFolderType, + const char* aSaveURI, bool* aResult); + +#endif /* _nsMsgCopy_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgPrompts.cpp b/comm/mailnews/compose/src/nsMsgPrompts.cpp new file mode 100644 index 0000000000..ff0f133285 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgPrompts.cpp @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsMsgPrompts.h" + +#include "nsMsgCopy.h" +#include "nsIPrompt.h" +#include "nsIWindowWatcher.h" +#include "nsComposeStrings.h" +#include "nsIStringBundle.h" +#include "nsServiceManagerUtils.h" +#include "nsMsgUtils.h" +#include "mozilla/Components.h" +#include "nsIPromptService.h" +#include "nsEmbedCID.h" + +nsresult nsMsgGetMessageByName(const char* aName, nsString& aResult) { + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties", + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + return bundle->GetStringFromName(aName, aResult); +} + +static nsresult nsMsgBuildMessageByName(const char* aName, nsIFile* aFile, + nsString& aResult) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties", + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString path; + aFile->GetPath(path); + + AutoTArray<nsString, 1> params = {path}; + return bundle->FormatStringFromName(aName, params, aResult); +} + +nsresult nsMsgBuildMessageWithFile(nsIFile* aFile, nsString& aResult) { + return nsMsgBuildMessageByName("unableToOpenFile", aFile, aResult); +} + +nsresult nsMsgBuildMessageWithTmpFile(nsIFile* aFile, nsString& aResult) { + return nsMsgBuildMessageByName("unableToOpenTmpFile", aFile, aResult); +} + +nsresult nsMsgDisplayMessageByName(mozIDOMWindowProxy* window, + const char* aName, + const char16_t* windowTitle) { + nsString msg; + nsMsgGetMessageByName(aName, msg); + return nsMsgDisplayMessageByString(window, msg.get(), windowTitle); +} + +nsresult nsMsgDisplayMessageByString(mozIDOMWindowProxy* window, + const char16_t* msg, + const char16_t* windowTitle) { + NS_ENSURE_ARG_POINTER(msg); + + nsresult rv; + nsCOMPtr<nsIPromptService> dlgService( + do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + return dlgService->Alert(window, windowTitle, msg); +} + +nsresult nsMsgAskBooleanQuestionByString(mozIDOMWindowProxy* window, + const char16_t* msg, bool* answer, + const char16_t* windowTitle) { + NS_ENSURE_TRUE(msg && *msg, NS_ERROR_INVALID_ARG); + + nsresult rv; + nsCOMPtr<nsIPromptService> dlgService( + do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + return dlgService->Confirm(window, windowTitle, msg, answer); +} diff --git a/comm/mailnews/compose/src/nsMsgPrompts.h b/comm/mailnews/compose/src/nsMsgPrompts.h new file mode 100644 index 0000000000..300b6ffee2 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgPrompts.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgPrompts_H_ +#define _nsMsgPrompts_H_ + +#include "nscore.h" +#include "nsError.h" +#include "nsString.h" + +class mozIDOMWindowProxy; + +nsresult nsMsgGetMessageByName(const char* aName, nsString& aResult); +nsresult nsMsgBuildMessageWithFile(nsIFile* aFile, nsString& aResult); +nsresult nsMsgBuildMessageWithTmpFile(nsIFile* aFile, nsString& aResult); +nsresult nsMsgDisplayMessageByName(mozIDOMWindowProxy* window, + const char* aName, + const char16_t* windowTitle = nullptr); +nsresult nsMsgDisplayMessageByString(mozIDOMWindowProxy* window, + const char16_t* msg, + const char16_t* windowTitle = nullptr); +nsresult nsMsgAskBooleanQuestionByString(mozIDOMWindowProxy* window, + const char16_t* msg, bool* answer, + const char16_t* windowTitle = nullptr); + +#endif /* _nsMsgPrompts_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgQuote.cpp b/comm/mailnews/compose/src/nsMsgQuote.cpp new file mode 100644 index 0000000000..75efbf67ca --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgQuote.cpp @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsIURL.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIServiceManager.h" +#include "nsIStreamListener.h" +#include "nsIStreamConverter.h" +#include "nsIStreamConverterService.h" +#include "nsIMimeStreamConverter.h" +#include "nsMimeTypes.h" +#include "nsICharsetConverterManager.h" +#include "prprf.h" +#include "nsMsgQuote.h" +#include "nsMsgCompUtils.h" +#include "nsIMsgMessageService.h" +#include "nsMsgUtils.h" +#include "nsNetUtil.h" +#include "nsMsgCompose.h" +#include "nsMsgMailNewsUrl.h" +#include "mozilla/Components.h" +#include "nsContentUtils.h" + +NS_IMPL_ISUPPORTS(nsMsgQuoteListener, nsIMsgQuoteListener, + nsIMimeStreamConverterListener) + +nsMsgQuoteListener::nsMsgQuoteListener() {} + +nsMsgQuoteListener::~nsMsgQuoteListener() {} + +NS_IMETHODIMP nsMsgQuoteListener::SetMsgQuote(nsIMsgQuote* msgQuote) { + mMsgQuote = do_GetWeakReference(msgQuote); + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuoteListener::GetMsgQuote(nsIMsgQuote** aMsgQuote) { + nsresult rv = NS_OK; + if (aMsgQuote) { + nsCOMPtr<nsIMsgQuote> msgQuote = do_QueryReferent(mMsgQuote); + msgQuote.forget(aMsgQuote); + } else + rv = NS_ERROR_NULL_POINTER; + + return rv; +} + +nsresult nsMsgQuoteListener::OnHeadersReady(nsIMimeHeaders* headers) { + nsCOMPtr<nsIMsgQuotingOutputStreamListener> quotingOutputStreamListener; + nsCOMPtr<nsIMsgQuote> msgQuote = do_QueryReferent(mMsgQuote); + + if (msgQuote) + msgQuote->GetStreamListener(getter_AddRefs(quotingOutputStreamListener)); + + if (quotingOutputStreamListener) + quotingOutputStreamListener->SetMimeHeaders(headers); + return NS_OK; +} + +// +// Implementation... +// +nsMsgQuote::nsMsgQuote() { + mQuoteHeaders = false; + mQuoteListener = nullptr; +} + +nsMsgQuote::~nsMsgQuote() {} + +NS_IMPL_ISUPPORTS(nsMsgQuote, nsIMsgQuote, nsISupportsWeakReference) + +NS_IMETHODIMP nsMsgQuote::GetStreamListener( + nsIMsgQuotingOutputStreamListener** aStreamListener) { + if (!aStreamListener) { + return NS_ERROR_NULL_POINTER; + } + nsCOMPtr<nsIMsgQuotingOutputStreamListener> streamListener = + do_QueryReferent(mStreamListener); + if (!streamListener) { + return NS_ERROR_FAILURE; + } + NS_IF_ADDREF(*aStreamListener = streamListener); + return NS_OK; +} + +nsresult nsMsgQuote::QuoteMessage( + const nsACString& msgURI, bool quoteHeaders, + nsIMsgQuotingOutputStreamListener* aQuoteMsgStreamListener, + bool aAutodetectCharset, bool headersOnly, nsIMsgDBHdr* aMsgHdr) { + nsresult rv; + + mQuoteHeaders = quoteHeaders; + mStreamListener = do_GetWeakReference(aQuoteMsgStreamListener); + + nsAutoCString msgUri(msgURI); + bool fileUrl = StringBeginsWith(msgUri, "file:"_ns); + bool forwardedMessage = msgUri.Find("&realtype=message/rfc822") >= 0; + nsCOMPtr<nsIURI> newURI; + if (fileUrl) { + msgUri.Replace(0, 5, "mailbox:"_ns); + msgUri.AppendLiteral("?number=0"); + rv = NS_NewURI(getter_AddRefs(newURI), msgUri); + } else if (forwardedMessage) + rv = NS_NewURI(getter_AddRefs(newURI), msgURI); + else { + nsCOMPtr<nsIMsgMessageService> msgService; + rv = GetMessageServiceFromURI(msgURI, getter_AddRefs(msgService)); + if (NS_FAILED(rv)) return rv; + rv = msgService->GetUrlForUri(msgURI, nullptr, getter_AddRefs(newURI)); + } + if (NS_FAILED(rv)) return rv; + + nsAutoCString queryPart; + rv = newURI->GetQuery(queryPart); + if (!queryPart.IsEmpty()) queryPart.Append('&'); + + if (headersOnly) /* We don't need to quote the message body but we still need + to extract the headers */ + queryPart.AppendLiteral("header=only"); + else if (quoteHeaders) + queryPart.AppendLiteral("header=quote"); + else + queryPart.AppendLiteral("header=quotebody"); + rv = NS_MutateURI(newURI).SetQuery(queryPart).Finalize(newURI); + NS_ENSURE_SUCCESS(rv, rv); + + // if we were told to auto-detect the charset, pass that on. + if (aAutodetectCharset) { + nsCOMPtr<nsIMsgI18NUrl> i18nUrl(do_QueryInterface(newURI)); + if (i18nUrl) i18nUrl->SetAutodetectCharset(true); + } + + mQuoteListener = + do_CreateInstance("@mozilla.org/messengercompose/quotinglistener;1", &rv); + if (NS_FAILED(rv)) return rv; + mQuoteListener->SetMsgQuote(this); + + // funky magic go get the isupports for this class which inherits from + // multiple interfaces. + nsISupports* supports; + QueryInterface(NS_GET_IID(nsISupports), (void**)&supports); + nsCOMPtr<nsISupports> quoteSupport = supports; + NS_IF_RELEASE(supports); + + // now we want to create a necko channel for this url and we want to open it + mQuoteChannel = nullptr; + nsCOMPtr<nsIIOService> netService = mozilla::components::IO::Service(); + NS_ENSURE_TRUE(netService, NS_ERROR_UNEXPECTED); + rv = netService->NewChannelFromURI( + newURI, nullptr, nsContentUtils::GetSystemPrincipal(), nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, getter_AddRefs(mQuoteChannel)); + + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIStreamConverterService> streamConverterService = + do_GetService("@mozilla.org/streamConverters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStreamListener> convertedListener; + nsCOMPtr<nsIMsgQuotingOutputStreamListener> streamListener = + do_QueryReferent(mStreamListener); + rv = streamConverterService->AsyncConvertData( + "message/rfc822", "application/xhtml+xml", streamListener, quoteSupport, + getter_AddRefs(convertedListener)); + if (NS_FAILED(rv)) return rv; + + // now try to open the channel passing in our display consumer as the + // listener + rv = mQuoteChannel->AsyncOpen(convertedListener); + return rv; +} + +NS_IMETHODIMP +nsMsgQuote::GetQuoteListener(nsIMimeStreamConverterListener** aQuoteListener) { + if (!aQuoteListener || !mQuoteListener) return NS_ERROR_NULL_POINTER; + NS_ADDREF(*aQuoteListener = mQuoteListener); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuote::GetQuoteChannel(nsIChannel** aQuoteChannel) { + if (!aQuoteChannel || !mQuoteChannel) return NS_ERROR_NULL_POINTER; + NS_ADDREF(*aQuoteChannel = mQuoteChannel); + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgQuote.h b/comm/mailnews/compose/src/nsMsgQuote.h new file mode 100644 index 0000000000..27525a6dc1 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgQuote.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#ifndef __nsMsgQuote_h__ +#define __nsMsgQuote_h__ + +#include "nsIMsgQuote.h" +#include "nsIMsgMessageService.h" +#include "nsIStreamListener.h" +#include "nsIMimeStreamConverter.h" +#include "nsIChannel.h" +#include "nsCOMPtr.h" +#include "nsWeakReference.h" + +class nsMsgQuote; + +class nsMsgQuoteListener : public nsIMsgQuoteListener { + public: + nsMsgQuoteListener(); + + NS_DECL_THREADSAFE_ISUPPORTS + + // nsIMimeStreamConverterListener support + NS_DECL_NSIMIMESTREAMCONVERTERLISTENER + NS_DECL_NSIMSGQUOTELISTENER + + private: + virtual ~nsMsgQuoteListener(); + nsWeakPtr mMsgQuote; +}; + +class nsMsgQuote : public nsIMsgQuote, public nsSupportsWeakReference { + public: + nsMsgQuote(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGQUOTE + + private: + virtual ~nsMsgQuote(); + // + // Implementation data... + // + nsWeakPtr mStreamListener; + bool mQuoteHeaders; + nsCOMPtr<nsIMsgQuoteListener> mQuoteListener; + nsCOMPtr<nsIChannel> mQuoteChannel; +}; + +#endif /* __nsMsgQuote_h__ */ diff --git a/comm/mailnews/compose/src/nsMsgSendLater.cpp b/comm/mailnews/compose/src/nsMsgSendLater.cpp new file mode 100644 index 0000000000..8e3b8b0a5d --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgSendLater.cpp @@ -0,0 +1,1406 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ +#include "nsMsgSendLater.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsMsgCopy.h" +#include "nsIMsgSend.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIMsgMessageService.h" +#include "nsIMsgAccountManager.h" +#include "nsMsgCompUtils.h" +#include "nsMsgUtils.h" +#include "nsMailHeaders.h" +#include "nsMsgPrompts.h" +#include "nsISmtpUrl.h" +#include "nsIChannel.h" +#include "nsNetUtil.h" +#include "prlog.h" +#include "prmem.h" +#include "nsIMimeConverter.h" +#include "nsComposeStrings.h" +#include "nsIObserverService.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgDatabase.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIMsgWindow.h" +#include "nsMsgMessageFlags.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/Services.h" + +// Consts for checking and sending mail in milliseconds + +// 1 second from mail into the unsent messages folder to initially trying to +// send it. +const uint32_t kInitialMessageSendTime = 1000; + +NS_IMPL_ISUPPORTS(nsMsgSendLater, nsIMsgSendLater, nsIFolderListener, + nsIRequestObserver, nsIStreamListener, nsIObserver, + nsIUrlListener, nsIMsgShutdownTask) + +nsMsgSendLater::nsMsgSendLater() { + mSendingMessages = false; + mTimerSet = false; + mTotalSentSuccessfully = 0; + mTotalSendCount = 0; + mLeftoverBuffer = nullptr; + + m_to = nullptr; + m_bcc = nullptr; + m_fcc = nullptr; + m_newsgroups = nullptr; + m_newshost = nullptr; + m_headers = nullptr; + m_flags = 0; + m_headersFP = 0; + m_inhead = true; + m_headersPosition = 0; + + m_bytesRead = 0; + m_position = 0; + m_flagsPosition = 0; + m_headersSize = 0; + + mIdentityKey = nullptr; + mAccountKey = nullptr; + + mUserInitiated = false; +} + +nsMsgSendLater::~nsMsgSendLater() { + PR_Free(m_to); + PR_Free(m_fcc); + PR_Free(m_bcc); + PR_Free(m_newsgroups); + PR_Free(m_newshost); + PR_Free(m_headers); + PR_Free(mLeftoverBuffer); + PR_Free(mIdentityKey); + PR_Free(mAccountKey); +} + +nsresult nsMsgSendLater::Init() { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool sendInBackground; + rv = prefs->GetBoolPref("mailnews.sendInBackground", &sendInBackground); + // If we're not sending in the background, don't do anything else + if (NS_FAILED(rv) || !sendInBackground) return NS_OK; + + // We need to know when we're shutting down. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED); + + rv = observerService->AddObserver(this, "xpcom-shutdown", false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->AddObserver(this, "quit-application", false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->AddObserver(this, "msg-shutdown", false); + NS_ENSURE_SUCCESS(rv, rv); + + // Subscribe to the unsent messages folder + // XXX This code should be set up for multiple unsent folders, however we + // don't support that at the moment, so for now just assume one folder. + nsCOMPtr<nsIMsgFolder> folder; + rv = GetUnsentMessagesFolder(nullptr, getter_AddRefs(folder)); + // There doesn't have to be a nsMsgQueueForLater flagged folder. + if (NS_FAILED(rv) || !folder) return NS_OK; + + rv = folder->AddFolderListener(this); + NS_ENSURE_SUCCESS(rv, rv); + + // XXX may want to send messages X seconds after startup if there are any. + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (aSubject == mTimer && !strcmp(aTopic, "timer-callback")) { + if (mTimer) + mTimer->Cancel(); + else + NS_ERROR("mTimer was null in nsMsgSendLater::Observe"); + + mTimerSet = false; + // If we've already started a send since the timer fired, don't start + // another + if (!mSendingMessages) InternalSendMessages(false, nullptr); + } else if (!strcmp(aTopic, "quit-application")) { + // If the timer is set, cancel it - we're quitting, the shutdown service + // interfaces will sort out sending etc. + if (mTimer) mTimer->Cancel(); + + mTimerSet = false; + } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + // We're shutting down. Unsubscribe from the unsentFolder notifications + // they aren't any use to us now, we don't want to start sending more + // messages. + nsresult rv; + if (mMessageFolder) { + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv); + if (folder) { + rv = folder->RemoveFolderListener(this); + NS_ENSURE_SUCCESS(rv, rv); + folder->ForceDBClosed(); + } + } + + // Now remove ourselves from the observer service as well. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED); + + rv = observerService->RemoveObserver(this, "xpcom-shutdown"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->RemoveObserver(this, "quit-application"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->RemoveObserver(this, "msg-shutdown"); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::SetStatusFeedback(nsIMsgStatusFeedback* aFeedback) { + mFeedback = aFeedback; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::GetStatusFeedback(nsIMsgStatusFeedback** aFeedback) { + NS_ENSURE_ARG_POINTER(aFeedback); + NS_IF_ADDREF(*aFeedback = mFeedback); + return NS_OK; +} + +// Stream is done...drive on! +NS_IMETHODIMP +nsMsgSendLater::OnStopRequest(nsIRequest* request, nsresult status) { + nsresult rv; + + // First, this shouldn't happen, but if + // it does, flush the buffer and move on. + if (mLeftoverBuffer) { + DeliverQueuedLine(mLeftoverBuffer, PL_strlen(mLeftoverBuffer)); + } + + if (mOutFile) mOutFile->Close(); + + // See if we succeeded on reading the message from the message store? + // + if (NS_SUCCEEDED(status)) { + // Message is done...send it! + rv = CompleteMailFileSend(); + +#ifdef NS_DEBUG + printf("nsMsgSendLater: Success on getting message...\n"); +#endif + + // If the send operation failed..try the next one... + if (NS_FAILED(rv)) { + rv = StartNextMailFileSend(rv); + if (NS_FAILED(rv)) + EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully); + } + } else { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + if (!channel) return NS_ERROR_FAILURE; + + // extract the prompt object to use for the alert from the url.... + nsCOMPtr<nsIURI> uri; + nsCOMPtr<mozIDOMWindowProxy> domWindow; + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(uri)); + nsCOMPtr<nsIMsgWindow> msgWindow; + if (msgUrl) msgUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + if (msgWindow) msgWindow->GetDomWindow(getter_AddRefs(domWindow)); + } + + nsMsgDisplayMessageByName(domWindow, "errorQueuedDeliveryFailed"); + + // Getting the data failed, but we will still keep trying to send the + // rest... + rv = StartNextMailFileSend(status); + if (NS_FAILED(rv)) + EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully); + } + + return rv; +} + +char* FindEOL(char* inBuf, char* buf_end) { + char* buf = inBuf; + char* findLoc = nullptr; + + while (buf <= buf_end) + if (*buf == 0) + return buf; + else if ((*buf == '\n') || (*buf == '\r')) { + findLoc = buf; + break; + } else + ++buf; + + if (!findLoc) + return nullptr; + else if ((findLoc + 1) > buf_end) + return buf; + + if ((*findLoc == '\n' && *(findLoc + 1) == '\r') || + (*findLoc == '\r' && *(findLoc + 1) == '\n')) + findLoc++; // possibly a pair. + return findLoc; +} + +nsresult nsMsgSendLater::RebufferLeftovers(char* startBuf, uint32_t aLen) { + PR_FREEIF(mLeftoverBuffer); + mLeftoverBuffer = (char*)PR_Malloc(aLen + 1); + if (!mLeftoverBuffer) return NS_ERROR_OUT_OF_MEMORY; + + memcpy(mLeftoverBuffer, startBuf, aLen); + mLeftoverBuffer[aLen] = '\0'; + return NS_OK; +} + +nsresult nsMsgSendLater::BuildNewBuffer(const char* aBuf, uint32_t aCount, + uint32_t* totalBufSize) { + // Only build a buffer when there are leftovers... + if (!mLeftoverBuffer) { + return NS_ERROR_FAILURE; + } + + int32_t leftoverSize = PL_strlen(mLeftoverBuffer); + char* newBuffer = (char*)PR_Realloc(mLeftoverBuffer, aCount + leftoverSize); + NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY); + mLeftoverBuffer = newBuffer; + + memcpy(mLeftoverBuffer + leftoverSize, aBuf, aCount); + *totalBufSize = aCount + leftoverSize; + return NS_OK; +} + +// Got data? +NS_IMETHODIMP +nsMsgSendLater::OnDataAvailable(nsIRequest* request, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + NS_ENSURE_ARG_POINTER(inStr); + + // This is a little bit tricky since we have to chop random + // buffers into lines and deliver the lines...plus keeping the + // leftovers for next time...some fun, eh? + // + nsresult rv = NS_OK; + char* startBuf; + char* endBuf; + char* lineEnd; + char* newbuf = nullptr; + uint32_t size; + + uint32_t aCount = count; + char* aBuf = (char*)PR_Malloc(aCount + 1); + + inStr->Read(aBuf, count, &aCount); + + // First, create a new work buffer that will + if (NS_FAILED(BuildNewBuffer(aBuf, aCount, &size))) // no leftovers... + { + startBuf = (char*)aBuf; + endBuf = (char*)(aBuf + aCount - 1); + } else // yum, leftovers...new buffer created...sitting in mLeftoverBuffer + { + newbuf = mLeftoverBuffer; + startBuf = newbuf; + endBuf = startBuf + size - 1; + mLeftoverBuffer = nullptr; // null out this + } + + while (startBuf <= endBuf) { + lineEnd = FindEOL(startBuf, endBuf); + if (!lineEnd) { + rv = RebufferLeftovers(startBuf, (endBuf - startBuf) + 1); + break; + } + + rv = DeliverQueuedLine(startBuf, (lineEnd - startBuf) + 1); + if (NS_FAILED(rv)) break; + + startBuf = lineEnd + 1; + } + + PR_Free(newbuf); + PR_Free(aBuf); + return rv; +} + +NS_IMETHODIMP +nsMsgSendLater::OnStartRunningUrl(nsIURI* url) { return NS_OK; } + +NS_IMETHODIMP +nsMsgSendLater::OnStopRunningUrl(nsIURI* url, nsresult aExitCode) { + if (NS_SUCCEEDED(aExitCode)) InternalSendMessages(mUserInitiated, mIdentity); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnStartRequest(nsIRequest* request) { return NS_OK; } + +//////////////////////////////////////////////////////////////////////////////////// +// This is the listener class for the send operation. We have to create this +// class to listen for message send completion and eventually notify the caller +//////////////////////////////////////////////////////////////////////////////////// +NS_IMPL_ISUPPORTS(SendOperationListener, nsIMsgSendListener, + nsIMsgCopyServiceListener) + +SendOperationListener::SendOperationListener(nsMsgSendLater* aSendLater) + : mSendLater(aSendLater) {} + +SendOperationListener::~SendOperationListener(void) {} + +NS_IMETHODIMP +SendOperationListener::OnGetDraftFolderURI(const char* aMsgID, + const nsACString& aFolderURI) { + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::OnStartSending(const char* aMsgID, uint32_t aMsgSize) { +#ifdef NS_DEBUG + printf("SendOperationListener::OnStartSending()\n"); +#endif + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::OnProgress(const char* aMsgID, uint32_t aProgress, + uint32_t aProgressMax) { +#ifdef NS_DEBUG + printf("SendOperationListener::OnProgress()\n"); +#endif + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::OnStatus(const char* aMsgID, const char16_t* aMsg) { +#ifdef NS_DEBUG + printf("SendOperationListener::OnStatus()\n"); +#endif + + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::OnSendNotPerformed(const char* aMsgID, + nsresult aStatus) { + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::OnStopSending(const char* aMsgID, nsresult aStatus, + const char16_t* aMsg, + nsIFile* returnFile) { + if (mSendLater && !mSendLater->OnSendStepFinished(aStatus)) + mSendLater = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::OnTransportSecurityError( + const char* msgID, nsresult status, nsITransportSecurityInfo* secInfo, + nsACString const& location) { + return NS_OK; +} + +// nsIMsgCopyServiceListener + +NS_IMETHODIMP +SendOperationListener::OnStartCopy(void) { return NS_OK; } + +NS_IMETHODIMP +SendOperationListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { + return NS_OK; +} + +NS_IMETHODIMP +SendOperationListener::SetMessageKey(nsMsgKey aKey) { + MOZ_ASSERT_UNREACHABLE("SendOperationListener::SetMessageKey()"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SendOperationListener::GetMessageId(nsACString& messageId) { + MOZ_ASSERT_UNREACHABLE("SendOperationListener::GetMessageId()"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SendOperationListener::OnStopCopy(nsresult aStatus) { + if (mSendLater) { + mSendLater->OnCopyStepFinished(aStatus); + mSendLater = nullptr; + } + + return NS_OK; +} + +nsresult nsMsgSendLater::CompleteMailFileSend() { + // get the identity from the key + // if no key, or we fail to find the identity + // use the default identity on the default account + nsCOMPtr<nsIMsgIdentity> identity; + nsresult rv = GetIdentityFromKey(mIdentityKey, getter_AddRefs(identity)); + NS_ENSURE_SUCCESS(rv, rv); + if (!identity) return NS_ERROR_UNEXPECTED; + + // If for some reason the tmp file didn't get created, we've failed here + bool created; + mTempFile->Exists(&created); + if (!created) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMsgCompFields> compFields = + do_CreateInstance("@mozilla.org/messengercompose/composefields;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgSend> pMsgSend = + do_CreateInstance("@mozilla.org/messengercompose/send;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Since we have already parsed all of the headers, we are simply going to + // set the composition fields and move on. + nsCString author; + mMessage->GetAuthor(getter_Copies(author)); + + nsMsgCompFields* fields = (nsMsgCompFields*)compFields.get(); + + fields->SetFrom(author.get()); + + if (m_to) { + fields->SetTo(m_to); + } + + if (m_bcc) { + fields->SetBcc(m_bcc); + } + + if (m_fcc) { + fields->SetFcc(m_fcc); + } + + if (m_newsgroups) fields->SetNewsgroups(m_newsgroups); + +#if 0 + // needs cleanup. Is this needed? + if (m_newshost) + fields->SetNewspostUrl(m_newshost); +#endif + + // Create the listener for the send operation... + RefPtr<SendOperationListener> sendListener = new SendOperationListener(this); + + RefPtr<mozilla::dom::Promise> promise; + rv = pMsgSend->SendMessageFile( + identity, mAccountKey, + compFields, // nsIMsgCompFields *fields, + mTempFile, // nsIFile *sendFile, + true, // bool deleteSendFileOnCompletion, + false, // bool digest_p, + nsIMsgSend::nsMsgSendUnsent, // nsMsgDeliverMode mode, + nullptr, // nsIMsgDBHdr *msgToReplace, + sendListener, mFeedback, nullptr, getter_AddRefs(promise)); + return rv; +} + +nsresult nsMsgSendLater::StartNextMailFileSend(nsresult prevStatus) { + if (mTotalSendCount >= (uint32_t)mMessagesToSend.Count()) { + // Notify that this message has finished being sent. + NotifyListenersOnProgress(mTotalSendCount, mMessagesToSend.Count(), 100, + 100); + + // EndSendMessages resets everything for us + EndSendMessages(prevStatus, nullptr, mTotalSendCount, + mTotalSentSuccessfully); + + // XXX Should we be releasing references so that we don't hold onto items + // unnecessarily. + return NS_OK; + } + + // If we've already sent a message, and are sending more, send out a progress + // update with 100% for both send and copy as we must have finished by now. + if (mTotalSendCount > 0) { + NotifyListenersOnProgress(mTotalSendCount, mMessagesToSend.Count(), 100, + 100); + } + + mMessage = mMessagesToSend[mTotalSendCount++]; + + if (!mMessageFolder) return NS_ERROR_UNEXPECTED; + + nsCString messageURI; + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + folder->GetUriForMsg(mMessage, messageURI); + + rv = nsMsgCreateTempFile("nsqmail.tmp", getter_AddRefs(mTempFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgMessageService> messageService; + rv = GetMessageServiceFromURI(messageURI, getter_AddRefs(messageService)); + if (NS_FAILED(rv) && !messageService) return NS_ERROR_FACTORY_NOT_LOADED; + + nsCString identityKey; + rv = mMessage->GetStringProperty(HEADER_X_MOZILLA_IDENTITY_KEY, identityKey); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIdentity> identity; + rv = GetIdentityFromKey(identityKey.get(), getter_AddRefs(identity)); + NS_ENSURE_SUCCESS(rv, rv); + if (!identity) return NS_ERROR_UNEXPECTED; + + // Notify that we're just about to start sending this message + NotifyListenersOnMessageStartSending(mTotalSendCount, mMessagesToSend.Count(), + identity); + + // Setup what we need to parse the data stream correctly + m_inhead = true; + m_headersFP = 0; + m_headersPosition = 0; + m_bytesRead = 0; + m_position = 0; + m_flagsPosition = 0; + m_headersSize = 0; + PR_FREEIF(mLeftoverBuffer); + + // Now, get our stream listener interface and plug it into the LoadMessage + // operation + rv = messageService->LoadMessage(messageURI, + static_cast<nsIStreamListener*>(this), + nullptr, nullptr, false); + + return rv; +} + +NS_IMETHODIMP +nsMsgSendLater::GetUnsentMessagesFolder(nsIMsgIdentity* aIdentity, + nsIMsgFolder** aFolder) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder); + if (!folder) { + nsCString uri; + GetFolderURIFromUserPrefs(nsIMsgSend::nsMsgQueueForLater, aIdentity, uri); + rv = LocateMessageFolder(aIdentity, nsIMsgSend::nsMsgQueueForLater, + uri.get(), getter_AddRefs(folder)); + mMessageFolder = do_GetWeakReference(folder); + if (!mMessageFolder) return NS_ERROR_FAILURE; + } + if (folder) folder.forget(aFolder); + return rv; +} + +NS_IMETHODIMP +nsMsgSendLater::HasUnsentMessages(nsIMsgIdentity* aIdentity, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + nsresult rv; + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgAccount>> accounts; + rv = accountManager->GetAccounts(accounts); + NS_ENSURE_SUCCESS(rv, rv); + + if (accounts.IsEmpty()) + return NS_OK; // no account set up -> no unsent messages + + // XXX This code should be set up for multiple unsent folders, however we + // don't support that at the moment, so for now just assume one folder. + if (!mMessageFolder) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetUnsentMessagesFolder(nullptr, getter_AddRefs(folder)); + // There doesn't have to be a nsMsgQueueForLater flagged folder. + if (NS_FAILED(rv) || !folder) return NS_OK; + } + rv = ReparseDBIfNeeded(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t totalMessages; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = folder->GetTotalMessages(false, &totalMessages); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = totalMessages > 0; + return NS_OK; +} + +// +// To really finalize this capability, we need to have the ability to get +// the message from the mail store in a stream for processing. The flow +// would be something like this: +// +// foreach (message in Outbox folder) +// get stream of Nth message +// if (not done with headers) +// Tack on to current buffer of headers +// when done with headers +// BuildHeaders() +// Write Headers to Temp File +// after done with headers +// write rest of message body to temp file +// +// when done with the message +// do send operation +// +// when send is complete +// Copy from Outbox to FCC folder +// Delete from Outbox folder +// +// +NS_IMETHODIMP +nsMsgSendLater::SendUnsentMessages(nsIMsgIdentity* aIdentity) { + return InternalSendMessages(true, aIdentity); +} + +// Returns NS_OK if the db is OK, an error otherwise, e.g., we had to reparse. +nsresult nsMsgSendLater::ReparseDBIfNeeded(nsIUrlListener* aListener) { + // This will kick off a reparse, if needed. So the next time we check if + // there are unsent messages, the db will be up to date. + nsCOMPtr<nsIMsgDatabase> unsentDB; + nsresult rv; + nsCOMPtr<nsIMsgLocalMailFolder> locFolder( + do_QueryReferent(mMessageFolder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return locFolder->GetDatabaseWithReparse(aListener, nullptr, + getter_AddRefs(unsentDB)); +} + +nsresult nsMsgSendLater::InternalSendMessages(bool aUserInitiated, + nsIMsgIdentity* aIdentity) { + if (WeAreOffline()) return NS_MSG_ERROR_OFFLINE; + + // Protect against being called whilst we're already sending. + if (mSendingMessages) { + NS_ERROR("nsMsgSendLater is already sending messages"); + return NS_ERROR_FAILURE; + } + + nsresult rv; + + // XXX This code should be set up for multiple unsent folders, however we + // don't support that at the moment, so for now just assume one folder. + if (!mMessageFolder) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetUnsentMessagesFolder(nullptr, getter_AddRefs(folder)); + if (NS_FAILED(rv) || !folder) return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIMsgDatabase> unsentDB; + // Remember these in case we need to reparse the db. + mUserInitiated = aUserInitiated; + mIdentity = aIdentity; + rv = ReparseDBIfNeeded(this); + NS_ENSURE_SUCCESS(rv, rv); + mIdentity = nullptr; // don't hold onto the identity since we're a service. + + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgEnumerator> enumerator; + rv = folder->GetMessages(getter_AddRefs(enumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + // Build mMessagesToSend array. + bool hasMoreElements = false; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreElements)) && + hasMoreElements) { + nsCOMPtr<nsIMsgDBHdr> messageHeader; + rv = enumerator->GetNext(getter_AddRefs(messageHeader)); + if (NS_SUCCEEDED(rv)) { + if (aUserInitiated) { + // If the user initiated the send, add all messages + mMessagesToSend.AppendObject(messageHeader); + } else { + // Else just send those that are NOT marked as Queued. + uint32_t flags; + rv = messageHeader->GetFlags(&flags); + if (NS_SUCCEEDED(rv) && !(flags & nsMsgMessageFlags::Queued)) + mMessagesToSend.AppendObject(messageHeader); + } + } + } + + // We're now sending messages so its time to signal that and reset our counts. + mSendingMessages = true; + mTotalSentSuccessfully = 0; + mTotalSendCount = 0; + + // Notify the listeners that we are starting a send. + NotifyListenersOnStartSending(mMessagesToSend.Count()); + + return StartNextMailFileSend(NS_OK); +} + +nsresult nsMsgSendLater::SetOrigMsgDisposition() { + if (!mMessage) return NS_ERROR_NULL_POINTER; + + // We're finished sending a queued message. We need to look at mMessage + // and see if we need to set replied/forwarded + // flags for the original message that this message might be a reply to + // or forward of. + nsCString originalMsgURIs; + nsCString queuedDisposition; + mMessage->GetStringProperty(ORIG_URI_PROPERTY, originalMsgURIs); + mMessage->GetStringProperty(QUEUED_DISPOSITION_PROPERTY, queuedDisposition); + if (!queuedDisposition.IsEmpty()) { + nsTArray<nsCString> uriArray; + ParseString(originalMsgURIs, ',', uriArray); + for (uint32_t i = 0; i < uriArray.Length(); i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgDBHdrFromURI(uriArray[i], getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (msgHdr) { + // get the folder for the message resource + nsCOMPtr<nsIMsgFolder> msgFolder; + msgHdr->GetFolder(getter_AddRefs(msgFolder)); + if (msgFolder) { + nsMsgDispositionState dispositionSetting = + nsIMsgFolder::nsMsgDispositionState_None; + if (queuedDisposition.EqualsLiteral("replied")) + dispositionSetting = nsIMsgFolder::nsMsgDispositionState_Replied; + else if (queuedDisposition.EqualsLiteral("forwarded")) + dispositionSetting = nsIMsgFolder::nsMsgDispositionState_Forwarded; + else if (queuedDisposition.EqualsLiteral("redirected")) + dispositionSetting = nsIMsgFolder::nsMsgDispositionState_Redirected; + + msgFolder->AddMessageDispositionState(msgHdr, dispositionSetting); + } + } + } + } + return NS_OK; +} + +nsresult nsMsgSendLater::DeleteCurrentMessage() { + if (!mMessage) { + NS_ERROR("nsMsgSendLater: Attempt to delete an already deleted message"); + return NS_OK; + } + + // Get the composition fields interface + if (!mMessageFolder) return NS_ERROR_UNEXPECTED; + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = folder->DeleteMessages({&*mMessage}, nullptr, true, false, nullptr, + false /*allowUndo*/); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // Null out the message so we don't try and delete it again. + mMessage = nullptr; + + return NS_OK; +} + +// +// This function parses the headers, and also deletes from the header block +// any headers which should not be delivered in mail, regardless of whether +// they were present in the queue file. Such headers include: BCC, FCC, +// Sender, X-Mozilla-Status, X-Mozilla-News-Host, and Content-Length. +// (Content-Length is for the disk file only, and must not be allowed to +// escape onto the network, since it depends on the local linebreak +// representation. Arguably, we could allow Lines to escape, but it's not +// required by NNTP.) +// +nsresult nsMsgSendLater::BuildHeaders() { + char* buf = m_headers; + char* buf_end = buf + m_headersFP; + + PR_FREEIF(m_to); + PR_FREEIF(m_bcc); + PR_FREEIF(m_newsgroups); + PR_FREEIF(m_newshost); + PR_FREEIF(m_fcc); + PR_FREEIF(mIdentityKey); + PR_FREEIF(mAccountKey); + m_flags = 0; + + while (buf < buf_end) { + bool prune_p = false; + bool do_flags_p = false; + char* colon = PL_strchr(buf, ':'); + char* end; + char* value = 0; + char** header = 0; + char* header_start = buf; + + if (!colon) break; + + end = colon; + while (end > buf && (*end == ' ' || *end == '\t')) end--; + + switch (buf[0]) { + case 'B': + case 'b': + if (!PL_strncasecmp("BCC", buf, end - buf)) { + header = &m_bcc; + } + break; + case 'C': + case 'c': + if (!PL_strncasecmp("CC", buf, end - buf)) + header = &m_to; + else if (!PL_strncasecmp(HEADER_CONTENT_LENGTH, buf, end - buf)) + prune_p = true; + break; + case 'F': + case 'f': + if (!PL_strncasecmp("FCC", buf, end - buf)) { + header = &m_fcc; + prune_p = true; + } + break; + case 'L': + case 'l': + if (!PL_strncasecmp("Lines", buf, end - buf)) prune_p = true; + break; + case 'N': + case 'n': + if (!PL_strncasecmp("Newsgroups", buf, end - buf)) + header = &m_newsgroups; + break; + case 'S': + case 's': + if (!PL_strncasecmp("Sender", buf, end - buf)) prune_p = true; + break; + case 'T': + case 't': + if (!PL_strncasecmp("To", buf, end - buf)) header = &m_to; + break; + case 'X': + case 'x': { + if (buf + strlen(HEADER_X_MOZILLA_STATUS2) == end && + !PL_strncasecmp(HEADER_X_MOZILLA_STATUS2, buf, end - buf)) + prune_p = true; + else if (buf + strlen(HEADER_X_MOZILLA_STATUS) == end && + !PL_strncasecmp(HEADER_X_MOZILLA_STATUS, buf, end - buf)) + prune_p = do_flags_p = true; + else if (!PL_strncasecmp(HEADER_X_MOZILLA_DRAFT_INFO, buf, end - buf)) + prune_p = true; + else if (!PL_strncasecmp(HEADER_X_MOZILLA_KEYWORDS, buf, end - buf)) + prune_p = true; + else if (!PL_strncasecmp(HEADER_X_MOZILLA_NEWSHOST, buf, end - buf)) { + prune_p = true; + header = &m_newshost; + } else if (!PL_strncasecmp(HEADER_X_MOZILLA_IDENTITY_KEY, buf, + end - buf)) { + prune_p = true; + header = &mIdentityKey; + } else if (!PL_strncasecmp(HEADER_X_MOZILLA_ACCOUNT_KEY, buf, + end - buf)) { + prune_p = true; + header = &mAccountKey; + } + break; + } + } + + buf = colon + 1; + while (*buf == ' ' || *buf == '\t') buf++; + + value = buf; + + SEARCH_NEWLINE: + while (*buf != 0 && *buf != '\r' && *buf != '\n') buf++; + + if (buf + 1 >= buf_end) + ; + // If "\r\n " or "\r\n\t" is next, that doesn't terminate the header. + else if (buf + 2 < buf_end && (buf[0] == '\r' && buf[1] == '\n') && + (buf[2] == ' ' || buf[2] == '\t')) { + buf += 3; + goto SEARCH_NEWLINE; + } + // If "\r " or "\r\t" or "\n " or "\n\t" is next, that doesn't terminate + // the header either. + else if ((buf[0] == '\r' || buf[0] == '\n') && + (buf[1] == ' ' || buf[1] == '\t')) { + buf += 2; + goto SEARCH_NEWLINE; + } + + if (header) { + int L = buf - value; + if (*header) { + char* newh = (char*)PR_Realloc((*header), PL_strlen(*header) + L + 10); + if (!newh) return NS_ERROR_OUT_OF_MEMORY; + *header = newh; + newh = (*header) + PL_strlen(*header); + *newh++ = ','; + *newh++ = ' '; + memcpy(newh, value, L); + newh[L] = 0; + } else { + *header = (char*)PR_Malloc(L + 1); + if (!*header) return NS_ERROR_OUT_OF_MEMORY; + memcpy((*header), value, L); + (*header)[L] = 0; + } + } else if (do_flags_p) { + char* s = value; + PR_ASSERT(*s != ' ' && *s != '\t'); + NS_ASSERTION(MsgIsHex(s, 4), "Expected 4 hex digits for flags."); + m_flags = MsgUnhex(s, 4); + } + + if (*buf == '\r' || *buf == '\n') { + if (*buf == '\r' && buf[1] == '\n') buf++; + buf++; + } + + if (prune_p) { + char* to = header_start; + char* from = buf; + while (from < buf_end) *to++ = *from++; + buf = header_start; + buf_end = to; + m_headersFP = buf_end - m_headers; + } + } + + m_headers[m_headersFP++] = '\r'; + m_headers[m_headersFP++] = '\n'; + + // Now we have parsed out all of the headers we need and we + // can proceed. + return NS_OK; +} + +nsresult DoGrowBuffer(int32_t desired_size, int32_t element_size, + int32_t quantum, char** buffer, int32_t* size) { + if (*size <= desired_size) { + char* new_buf; + int32_t increment = desired_size - *size; + if (increment < quantum) // always grow by a minimum of N bytes + increment = quantum; + + new_buf = + (*buffer ? (char*)PR_Realloc(*buffer, (*size + increment) * + (element_size / sizeof(char))) + : (char*)PR_Malloc((*size + increment) * + (element_size / sizeof(char)))); + if (!new_buf) return NS_ERROR_OUT_OF_MEMORY; + *buffer = new_buf; + *size += increment; + } + return NS_OK; +} + +#define do_grow_headers(desired_size) \ + (((desired_size) >= m_headersSize) \ + ? DoGrowBuffer((desired_size), sizeof(char), 1024, &m_headers, \ + &m_headersSize) \ + : NS_OK) + +nsresult nsMsgSendLater::DeliverQueuedLine(const char* line, int32_t length) { + int32_t flength = length; + + m_bytesRead += length; + + // convert existing newline to CRLF + // Don't need this because the calling routine is taking care of it. + // if (length > 0 && (line[length-1] == '\r' || + // (line[length-1] == '\n' && (length < 2 || line[length-2] != '\r')))) + // { + // line[length-1] = '\r'; + // line[length++] = '\n'; + // } + // + // + // We are going to check if we are looking at a "From - " line. If so, + // then just eat it and return NS_OK + // + if (!PL_strncasecmp(line, "From - ", 7)) return NS_OK; + + if (m_inhead) { + if (m_headersPosition == 0) { + // This line is the first line in a header block. + // Remember its position. + m_headersPosition = m_position; + + // Also, since we're now processing the headers, clear out the + // slots which we will parse data into, so that the values that + // were used the last time around do not persist. + + // We must do that here, and not in the previous clause of this + // `else' (the "I've just seen a `From ' line clause") because + // that clause happens before delivery of the previous message is + // complete, whereas this clause happens after the previous msg + // has been delivered. If we did this up there, then only the + // last message in the folder would ever be able to be both + // mailed and posted (or fcc'ed.) + PR_FREEIF(m_to); + PR_FREEIF(m_bcc); + PR_FREEIF(m_newsgroups); + PR_FREEIF(m_newshost); + PR_FREEIF(m_fcc); + PR_FREEIF(mIdentityKey); + } + + if (line[0] == '\r' || line[0] == '\n' || line[0] == 0) { + // End of headers. Now parse them; open the temp file; + // and write the appropriate subset of the headers out. + m_inhead = false; + + nsresult rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mOutFile), + mTempFile, -1, 00600); + if (NS_FAILED(rv)) return NS_MSG_ERROR_WRITING_FILE; + + nsresult status = BuildHeaders(); + if (NS_FAILED(status)) return status; + + uint32_t n; + rv = mOutFile->Write(m_headers, m_headersFP, &n); + if (NS_FAILED(rv) || n != (uint32_t)m_headersFP) + return NS_MSG_ERROR_WRITING_FILE; + } else { + // Otherwise, this line belongs to a header. So append it to the + // header data. + + if (!PL_strncasecmp(line, HEADER_X_MOZILLA_STATUS, + PL_strlen(HEADER_X_MOZILLA_STATUS))) + // Notice the position of the flags. + m_flagsPosition = m_position; + else if (m_headersFP == 0) + m_flagsPosition = 0; + + nsresult status = do_grow_headers(length + m_headersFP + 10); + if (NS_FAILED(status)) return status; + + memcpy(m_headers + m_headersFP, line, length); + m_headersFP += length; + } + } else { + // This is a body line. Write it to the file. + PR_ASSERT(mOutFile); + if (mOutFile) { + uint32_t wrote; + nsresult rv = mOutFile->Write(line, length, &wrote); + if (NS_FAILED(rv) || wrote < (uint32_t)length) + return NS_MSG_ERROR_WRITING_FILE; + } + } + + m_position += flength; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::AddListener(nsIMsgSendLaterListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + mListenerArray.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::RemoveListener(nsIMsgSendLaterListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + return mListenerArray.RemoveElement(aListener) ? NS_OK : NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMsgSendLater::GetSendingMessages(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mSendingMessages; + return NS_OK; +} + +#define NOTIFY_LISTENERS(propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<nsCOMPtr<nsIMsgSendLaterListener>>::ForwardIterator iter( \ + mListenerArray); \ + nsCOMPtr<nsIMsgSendLaterListener> listener; \ + while (iter.HasMore()) { \ + listener = iter.GetNext(); \ + listener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +void nsMsgSendLater::NotifyListenersOnStartSending( + uint32_t aTotalMessageCount) { + NOTIFY_LISTENERS(OnStartSending, (aTotalMessageCount)); +} + +void nsMsgSendLater::NotifyListenersOnMessageStartSending( + uint32_t aCurrentMessage, uint32_t aTotalMessage, + nsIMsgIdentity* aIdentity) { + NOTIFY_LISTENERS(OnMessageStartSending, + (aCurrentMessage, aTotalMessage, mMessage, aIdentity)); +} + +void nsMsgSendLater::NotifyListenersOnProgress(uint32_t aCurrentMessage, + uint32_t aTotalMessage, + uint32_t aSendPercent, + uint32_t aCopyPercent) { + NOTIFY_LISTENERS(OnMessageSendProgress, (aCurrentMessage, aTotalMessage, + aSendPercent, aCopyPercent)); +} + +void nsMsgSendLater::NotifyListenersOnMessageSendError(uint32_t aCurrentMessage, + nsresult aStatus, + const char16_t* aMsg) { + NOTIFY_LISTENERS(OnMessageSendError, + (aCurrentMessage, mMessage, aStatus, aMsg)); +} + +/** + * This function is called to end sending of messages, it resets the send later + * system and notifies the relevant parties that we have finished. + */ +void nsMsgSendLater::EndSendMessages(nsresult aStatus, const char16_t* aMsg, + uint32_t aTotalTried, + uint32_t aSuccessful) { + // Catch-all, we may have had an issue sending, so we may not be calling + // StartNextMailFileSend to fully finish the sending. Therefore set + // mSendingMessages to false here so that we don't think we're still trying + // to send messages + mSendingMessages = false; + + // Clear out our array of messages. + mMessagesToSend.Clear(); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mMessageFolder, &rv); + NS_ENSURE_SUCCESS(rv, ); + // We don't need to keep hold of the database now we've finished sending. + (void)folder->SetMsgDatabase(nullptr); + + // or temp file or output stream + mTempFile = nullptr; + mOutFile = nullptr; + + NOTIFY_LISTENERS(OnStopSending, (aStatus, aMsg, aTotalTried, aSuccessful)); + + // If we've got a shutdown listener, notify it that we've finished. + if (mShutdownListener) { + mShutdownListener->OnStopRunningUrl(nullptr, NS_OK); + mShutdownListener = nullptr; + } +} + +/** + * Called when the send part of sending a message is finished. This will set up + * for the next step or "end" depending on the status. + * + * @param aStatus The success or fail result of the send step. + * @return True if the copy process will continue, false otherwise. + */ +bool nsMsgSendLater::OnSendStepFinished(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + SetOrigMsgDisposition(); + DeleteCurrentMessage(); + + // Send finished, so that is now 100%, copy to proceed... + NotifyListenersOnProgress(mTotalSendCount, mMessagesToSend.Count(), 100, 0); + + ++mTotalSentSuccessfully; + return true; + } else { + // XXX we don't currently get a message string from the send service. + NotifyListenersOnMessageSendError(mTotalSendCount, aStatus, nullptr); + nsresult rv = StartNextMailFileSend(aStatus); + // if this is the last message we're sending, we should report + // the status failure. + if (NS_FAILED(rv)) + EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully); + } + return false; +} + +/** + * Called when the copy part of sending a message is finished. This will send + * the next message or handle failure as appropriate. + * + * @param aStatus The success or fail result of the copy step. + */ +void nsMsgSendLater::OnCopyStepFinished(nsresult aStatus) { + // Regardless of the success of the copy we will still keep trying + // to send the rest... + nsresult rv = StartNextMailFileSend(aStatus); + if (NS_FAILED(rv)) + EndSendMessages(rv, nullptr, mTotalSendCount, mTotalSentSuccessfully); +} + +// XXX todo +// maybe this should just live in the account manager? +nsresult nsMsgSendLater::GetIdentityFromKey(const char* aKey, + nsIMsgIdentity** aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (aKey) { + nsTArray<RefPtr<nsIMsgIdentity>> identities; + if (NS_SUCCEEDED(accountManager->GetAllIdentities(identities))) { + for (auto lookupIdentity : identities) { + nsCString key; + lookupIdentity->GetKey(key); + if (key.Equals(aKey)) { + lookupIdentity.forget(aIdentity); + return NS_OK; + } + } + } + } + + // If no aKey, or we failed to find the identity from the key + // use the identity from the default account. + nsCOMPtr<nsIMsgAccount> defaultAccount; + rv = accountManager->GetDefaultAccount(getter_AddRefs(defaultAccount)); + NS_ENSURE_SUCCESS(rv, rv); + + if (defaultAccount) + rv = defaultAccount->GetDefaultIdentity(aIdentity); + else + *aIdentity = nullptr; + + return rv; +} + +nsresult nsMsgSendLater::StartTimer() { + // No need to trigger if timer is already set + if (mTimerSet) return NS_OK; + + // XXX only trigger for non-queued headers + + // Items from this function return NS_OK because the callee won't care about + // the result anyway. + nsresult rv; + if (!mTimer) { + mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, NS_OK); + } + + rv = mTimer->Init(static_cast<nsIObserver*>(this), kInitialMessageSendTime, + nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, NS_OK); + + mTimerSet = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderAdded(nsIMsgFolder* /*parent*/, + nsIMsgFolder* /*child*/) { + return StartTimer(); +} + +NS_IMETHODIMP +nsMsgSendLater::OnMessageAdded(nsIMsgFolder* /*parent*/, nsIMsgDBHdr* /*msg*/) { + return StartTimer(); +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderRemoved(nsIMsgFolder* /*parent*/, + nsIMsgFolder* /*child*/) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnMessageRemoved(nsIMsgFolder* /*parent*/, + nsIMsgDBHdr* /*msg*/) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderPropertyChanged(nsIMsgFolder* aFolder, + const nsACString& aProperty, + const nsACString& aOldValue, + const nsACString& aNewValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderIntPropertyChanged(nsIMsgFolder* aFolder, + const nsACString& aProperty, + int64_t aOldValue, + int64_t aNewValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderBoolPropertyChanged(nsIMsgFolder* aFolder, + const nsACString& aProperty, + bool aOldValue, bool aNewValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderUnicharPropertyChanged(nsIMsgFolder* aFolder, + const nsACString& aProperty, + const nsAString& aOldValue, + const nsAString& aNewValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderPropertyFlagChanged(nsIMsgDBHdr* aMsg, + const nsACString& aProperty, + uint32_t aOldValue, + uint32_t aNewValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::OnFolderEvent(nsIMsgFolder* aFolder, const nsACString& aEvent) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::GetNeedsToRunTask(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mSendingMessages; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSendLater::DoShutdownTask(nsIUrlListener* aListener, nsIMsgWindow* aWindow, + bool* aResult) { + if (mTimer) mTimer->Cancel(); + // If we're already sending messages, nothing to do, but save the shutdown + // listener until we've finished. + if (mSendingMessages) { + mShutdownListener = aListener; + return NS_OK; + } + // Else we have pending messages, we need to throw up a dialog to find out + // if to send them or not. + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgSendLater::GetCurrentTaskName(nsAString& aResult) { + // XXX Bug 440794 will localize this, left as non-localized whilst we decide + // on the actual strings and try out the UI. + aResult = u"Sending Messages"_ns; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgSendLater.h b/comm/mailnews/compose/src/nsMsgSendLater.h new file mode 100644 index 0000000000..fcd8c53e38 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgSendLater.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgSendLater_H_ +#define _nsMsgSendLater_H_ + +#include "nsCOMArray.h" +#include "nsIMsgFolder.h" +#include "nsIMsgSendListener.h" +#include "nsIMsgSendLaterListener.h" +#include "nsIMsgSendLater.h" +#include "nsIMsgStatusFeedback.h" +#include "nsTObserverArray.h" +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsCOMPtr.h" +#include "nsIMsgShutdown.h" +#include "nsIWeakReferenceUtils.h" + +//////////////////////////////////////////////////////////////////////////////////// +// This is the listener class for the send operation. We have to create this +// class to listen for message send completion and eventually notify the caller +//////////////////////////////////////////////////////////////////////////////////// +class nsMsgSendLater; + +class SendOperationListener : public nsIMsgSendListener, + public nsIMsgCopyServiceListener { + public: + explicit SendOperationListener(nsMsgSendLater* aSendLater); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSENDLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + + private: + virtual ~SendOperationListener(); + RefPtr<nsMsgSendLater> mSendLater; +}; + +class nsMsgSendLater : public nsIMsgSendLater, + public nsIFolderListener, + public nsIObserver, + public nsIUrlListener, + public nsIMsgShutdownTask + +{ + public: + nsMsgSendLater(); + nsresult Init(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSENDLATER + NS_DECL_NSIFOLDERLISTENER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIOBSERVER + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGSHUTDOWNTASK + + // Methods needed for implementing interface... + nsresult StartNextMailFileSend(nsresult prevStatus); + nsresult CompleteMailFileSend(); + + nsresult DeleteCurrentMessage(); + nsresult SetOrigMsgDisposition(); + // Necessary for creating a valid list of recipients + nsresult BuildHeaders(); + nsresult DeliverQueuedLine(const char* line, int32_t length); + nsresult RebufferLeftovers(char* startBuf, uint32_t aLen); + nsresult BuildNewBuffer(const char* aBuf, uint32_t aCount, + uint32_t* totalBufSize); + + // methods for listener array processing... + void NotifyListenersOnStartSending(uint32_t aTotalMessageCount); + void NotifyListenersOnMessageStartSending(uint32_t aCurrentMessage, + uint32_t aTotalMessage, + nsIMsgIdentity* aIdentity); + void NotifyListenersOnProgress(uint32_t aCurrentMessage, + uint32_t aTotalMessage, uint32_t aSendPercent, + uint32_t aCopyPercent); + void NotifyListenersOnMessageSendError(uint32_t aCurrentMessage, + nsresult aStatus, + const char16_t* aMsg); + void EndSendMessages(nsresult aStatus, const char16_t* aMsg, + uint32_t aTotalTried, uint32_t aSuccessful); + + bool OnSendStepFinished(nsresult aStatus); + void OnCopyStepFinished(nsresult aStatus); + + private: + // counters and things for enumeration + uint32_t mTotalSentSuccessfully; + uint32_t mTotalSendCount; + nsCOMArray<nsIMsgDBHdr> mMessagesToSend; + nsWeakPtr mMessageFolder; + nsCOMPtr<nsIMsgStatusFeedback> mFeedback; + + virtual ~nsMsgSendLater(); + nsresult GetIdentityFromKey(const char* aKey, nsIMsgIdentity** aIdentity); + nsresult ReparseDBIfNeeded(nsIUrlListener* aListener); + nsresult InternalSendMessages(bool aUserInitiated, nsIMsgIdentity* aIdentity); + nsresult StartTimer(); + + nsTObserverArray<nsCOMPtr<nsIMsgSendLaterListener> > mListenerArray; + nsCOMPtr<nsIMsgDBHdr> mMessage; + nsCOMPtr<nsITimer> mTimer; + bool mTimerSet; + nsCOMPtr<nsIUrlListener> mShutdownListener; + + // + // File output stuff... + // + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIOutputStream> mOutFile; + + // For building headers and stream parsing... + char* m_to; + char* m_bcc; + char* m_fcc; + char* m_newsgroups; + char* m_newshost; + char* m_headers; + int32_t m_flags; + int32_t m_headersFP; + bool m_inhead; + int32_t m_headersPosition; + int32_t m_bytesRead; + int32_t m_position; + int32_t m_flagsPosition; + int32_t m_headersSize; + char* mLeftoverBuffer; + char* mIdentityKey; + char* mAccountKey; + + bool mSendingMessages; + bool mUserInitiated; + nsCOMPtr<nsIMsgIdentity> mIdentity; +}; + +#endif /* _nsMsgSendLater_H_ */ diff --git a/comm/mailnews/compose/src/nsMsgSendReport.cpp b/comm/mailnews/compose/src/nsMsgSendReport.cpp new file mode 100644 index 0000000000..5cab724810 --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgSendReport.cpp @@ -0,0 +1,385 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgSendReport.h" + +#include "msgCore.h" +#include "nsIMsgCompose.h" +#include "nsMsgPrompts.h" +#include "nsError.h" +#include "nsComposeStrings.h" +#include "nsIStringBundle.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Components.h" + +NS_IMPL_ISUPPORTS(nsMsgProcessReport, nsIMsgProcessReport) + +nsMsgProcessReport::nsMsgProcessReport() { Reset(); } + +nsMsgProcessReport::~nsMsgProcessReport() {} + +/* attribute boolean proceeded; */ +NS_IMETHODIMP nsMsgProcessReport::GetProceeded(bool* aProceeded) { + NS_ENSURE_ARG_POINTER(aProceeded); + *aProceeded = mProceeded; + return NS_OK; +} +NS_IMETHODIMP nsMsgProcessReport::SetProceeded(bool aProceeded) { + mProceeded = aProceeded; + return NS_OK; +} + +/* attribute nsresult error; */ +NS_IMETHODIMP nsMsgProcessReport::GetError(nsresult* aError) { + NS_ENSURE_ARG_POINTER(aError); + *aError = mError; + return NS_OK; +} +NS_IMETHODIMP nsMsgProcessReport::SetError(nsresult aError) { + mError = aError; + return NS_OK; +} + +/* attribute wstring message; */ +NS_IMETHODIMP nsMsgProcessReport::GetMessage(char16_t** aMessage) { + NS_ENSURE_ARG_POINTER(aMessage); + *aMessage = ToNewUnicode(mMessage); + return NS_OK; +} +NS_IMETHODIMP nsMsgProcessReport::SetMessage(const char16_t* aMessage) { + mMessage = aMessage; + return NS_OK; +} + +/* void Reset (); */ +NS_IMETHODIMP nsMsgProcessReport::Reset() { + mProceeded = false; + mError = NS_OK; + mMessage.Truncate(); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsMsgSendReport, nsIMsgSendReport) + +nsMsgSendReport::nsMsgSendReport() { + uint32_t i; + for (i = 0; i <= SEND_LAST_PROCESS; i++) + mProcessReport[i] = new nsMsgProcessReport(); + + Reset(); +} + +nsMsgSendReport::~nsMsgSendReport() { + uint32_t i; + for (i = 0; i <= SEND_LAST_PROCESS; i++) mProcessReport[i] = nullptr; +} + +/* attribute long currentProcess; */ +NS_IMETHODIMP nsMsgSendReport::GetCurrentProcess(int32_t* aCurrentProcess) { + NS_ENSURE_ARG_POINTER(aCurrentProcess); + *aCurrentProcess = mCurrentProcess; + return NS_OK; +} +NS_IMETHODIMP nsMsgSendReport::SetCurrentProcess(int32_t aCurrentProcess) { + if (aCurrentProcess < 0 || aCurrentProcess > SEND_LAST_PROCESS) + return NS_ERROR_ILLEGAL_VALUE; + + mCurrentProcess = aCurrentProcess; + if (mProcessReport[mCurrentProcess]) + mProcessReport[mCurrentProcess]->SetProceeded(true); + + return NS_OK; +} + +/* attribute long deliveryMode; */ +NS_IMETHODIMP nsMsgSendReport::GetDeliveryMode(int32_t* aDeliveryMode) { + NS_ENSURE_ARG_POINTER(aDeliveryMode); + *aDeliveryMode = mDeliveryMode; + return NS_OK; +} +NS_IMETHODIMP nsMsgSendReport::SetDeliveryMode(int32_t aDeliveryMode) { + mDeliveryMode = aDeliveryMode; + return NS_OK; +} + +/* void Reset (); */ +NS_IMETHODIMP nsMsgSendReport::Reset() { + uint32_t i; + for (i = 0; i <= SEND_LAST_PROCESS; i++) + if (mProcessReport[i]) mProcessReport[i]->Reset(); + + mCurrentProcess = 0; + mDeliveryMode = 0; + mAlreadyDisplayReport = false; + + return NS_OK; +} + +/* void setProceeded (in long process, in boolean proceeded); */ +NS_IMETHODIMP nsMsgSendReport::SetProceeded(int32_t process, bool proceeded) { + if (process < process_Current || process > SEND_LAST_PROCESS) + return NS_ERROR_ILLEGAL_VALUE; + + if (process == process_Current) process = mCurrentProcess; + + if (!mProcessReport[process]) return NS_ERROR_NOT_INITIALIZED; + + return mProcessReport[process]->SetProceeded(proceeded); +} + +/* void setError (in long process, in nsresult error, in boolean + * overwriteError); */ +NS_IMETHODIMP nsMsgSendReport::SetError(int32_t process, nsresult newError, + bool overwriteError) { + if (process < process_Current || process > SEND_LAST_PROCESS) + return NS_ERROR_ILLEGAL_VALUE; + + if (process == process_Current) { + if (mCurrentProcess == process_Current) + // We don't know what we're currently trying to do + return NS_ERROR_ILLEGAL_VALUE; + + process = mCurrentProcess; + } + + if (!mProcessReport[process]) return NS_ERROR_NOT_INITIALIZED; + + nsresult currError = NS_OK; + mProcessReport[process]->GetError(&currError); + if (overwriteError || NS_SUCCEEDED(currError)) + return mProcessReport[process]->SetError(newError); + else + return NS_OK; +} + +/* void setMessage (in long process, in wstring message, in boolean + * overwriteMessage); */ +NS_IMETHODIMP nsMsgSendReport::SetMessage(int32_t process, + const char16_t* message, + bool overwriteMessage) { + if (process < process_Current || process > SEND_LAST_PROCESS) + return NS_ERROR_ILLEGAL_VALUE; + + if (process == process_Current) { + if (mCurrentProcess == process_Current) + // We don't know what we're currently trying to do + return NS_ERROR_ILLEGAL_VALUE; + + process = mCurrentProcess; + } + + if (!mProcessReport[process]) return NS_ERROR_NOT_INITIALIZED; + + nsString currMessage; + mProcessReport[process]->GetMessage(getter_Copies(currMessage)); + if (overwriteMessage || currMessage.IsEmpty()) + return mProcessReport[process]->SetMessage(message); + else + return NS_OK; +} + +/* nsIMsgProcessReport getProcessReport (in long process); */ +NS_IMETHODIMP nsMsgSendReport::GetProcessReport(int32_t process, + nsIMsgProcessReport** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + if (process < process_Current || process > SEND_LAST_PROCESS) + return NS_ERROR_ILLEGAL_VALUE; + + if (process == process_Current) { + if (mCurrentProcess == process_Current) + // We don't know what we're currently trying to do + return NS_ERROR_ILLEGAL_VALUE; + + process = mCurrentProcess; + } + + NS_IF_ADDREF(*_retval = mProcessReport[process]); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSendReport::DisplayReport(mozIDOMWindowProxy* window, + bool showErrorOnly, + bool dontShowReportTwice, + nsresult* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + NS_ENSURE_TRUE(mCurrentProcess >= 0 && mCurrentProcess <= SEND_LAST_PROCESS, + NS_ERROR_NOT_INITIALIZED); + + nsresult currError = NS_OK; + mProcessReport[mCurrentProcess]->GetError(&currError); + *_retval = currError; + + if (dontShowReportTwice && mAlreadyDisplayReport) return NS_OK; + + if (showErrorOnly && NS_SUCCEEDED(currError)) return NS_OK; + + nsString currMessage; + mProcessReport[mCurrentProcess]->GetMessage(getter_Copies(currMessage)); + + nsresult rv; // don't step on currError. + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties", + getter_AddRefs(bundle)); + if (NS_FAILED(rv)) { + // TODO need to display a generic hardcoded message + mAlreadyDisplayReport = true; + return NS_OK; + } + + nsString dialogTitle; + nsString dialogMessage; + + if (NS_SUCCEEDED(currError)) { + // TODO display a success error message + return NS_OK; + } + + // Do we have an explanation of the error? if no, try to build one... + if (currMessage.IsEmpty()) { +#ifdef __GNUC__ +// Temporary workaround until bug 783526 is fixed. +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wswitch" +#endif + switch (currError) { + case NS_BINDING_ABORTED: + case NS_MSG_UNABLE_TO_SEND_LATER: + case NS_MSG_UNABLE_TO_SAVE_DRAFT: + case NS_MSG_UNABLE_TO_SAVE_TEMPLATE: + // Ignore, don't need to repeat ourself. + break; + default: + const char* errorString = errorStringNameForErrorCode(currError); + nsMsgGetMessageByName(errorString, currMessage); + break; + } +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + } + + if (mDeliveryMode == nsIMsgCompDeliverMode::Now || + mDeliveryMode == nsIMsgCompDeliverMode::SendUnsent) { + // SMTP is taking care of it's own error message and will return + // NS_ERROR_BUT_DONT_SHOW_ALERT as error code. In that case, we must not + // show an alert ourself. + if (currError == NS_ERROR_BUT_DONT_SHOW_ALERT) { + mAlreadyDisplayReport = true; + return NS_OK; + } + + bundle->GetStringFromName("sendMessageErrorTitle", dialogTitle); + + const char* preStrName = "sendFailed"; + bool askToGoBackToCompose = false; + switch (mCurrentProcess) { + case process_BuildMessage: + preStrName = "sendFailed"; + askToGoBackToCompose = false; + break; + case process_NNTP: + preStrName = "sendFailed"; + askToGoBackToCompose = false; + break; + case process_SMTP: + bool nntpProceeded; + mProcessReport[process_NNTP]->GetProceeded(&nntpProceeded); + if (nntpProceeded) + preStrName = "sendFailedButNntpOk"; + else + preStrName = "sendFailed"; + askToGoBackToCompose = false; + break; + case process_Copy: + preStrName = "failedCopyOperation"; + askToGoBackToCompose = (mDeliveryMode == nsIMsgCompDeliverMode::Now); + break; + case process_FCC: + preStrName = "failedCopyOperation"; + askToGoBackToCompose = (mDeliveryMode == nsIMsgCompDeliverMode::Now); + break; + } + bundle->GetStringFromName(preStrName, dialogMessage); + + // Do we already have an error message? + if (!askToGoBackToCompose && currMessage.IsEmpty()) { + // we don't have an error description but we can put a generic explanation + bundle->GetStringFromName("genericFailureExplanation", currMessage); + } + + if (!currMessage.IsEmpty()) { + // Don't need to repeat ourself! + if (!currMessage.Equals(dialogMessage)) { + if (!dialogMessage.IsEmpty()) dialogMessage.Append(char16_t('\n')); + dialogMessage.Append(currMessage); + } + } + + if (askToGoBackToCompose) { + bool oopsGiveMeBackTheComposeWindow = true; + nsString text1; + bundle->GetStringFromName("returnToComposeWindowQuestion", text1); + if (!dialogMessage.IsEmpty()) dialogMessage.AppendLiteral("\n"); + dialogMessage.Append(text1); + nsMsgAskBooleanQuestionByString(window, dialogMessage.get(), + &oopsGiveMeBackTheComposeWindow, + dialogTitle.get()); + if (!oopsGiveMeBackTheComposeWindow) *_retval = NS_OK; + } else + nsMsgDisplayMessageByString(window, dialogMessage.get(), + dialogTitle.get()); + } else { + const char* title; + const char* messageName; + + switch (mDeliveryMode) { + case nsIMsgCompDeliverMode::Later: + title = "sendLaterErrorTitle"; + messageName = "unableToSendLater"; + break; + + case nsIMsgCompDeliverMode::AutoSaveAsDraft: + case nsIMsgCompDeliverMode::SaveAsDraft: + title = "saveDraftErrorTitle"; + messageName = "unableToSaveDraft"; + break; + + case nsIMsgCompDeliverMode::SaveAsTemplate: + title = "saveTemplateErrorTitle"; + messageName = "unableToSaveTemplate"; + break; + + default: + /* This should never happen! */ + title = "sendMessageErrorTitle"; + messageName = "sendFailed"; + break; + } + + bundle->GetStringFromName(title, dialogTitle); + bundle->GetStringFromName(messageName, dialogMessage); + + // Do we have an error message... + if (currMessage.IsEmpty()) { + // we don't have an error description but we can put a generic explanation + bundle->GetStringFromName("genericFailureExplanation", currMessage); + } + + if (!currMessage.IsEmpty()) { + if (!dialogMessage.IsEmpty()) dialogMessage.Append(char16_t('\n')); + dialogMessage.Append(currMessage); + } + nsMsgDisplayMessageByString(window, dialogMessage.get(), dialogTitle.get()); + } + + mAlreadyDisplayReport = true; + return NS_OK; +} diff --git a/comm/mailnews/compose/src/nsMsgSendReport.h b/comm/mailnews/compose/src/nsMsgSendReport.h new file mode 100644 index 0000000000..0fcc2f1c9f --- /dev/null +++ b/comm/mailnews/compose/src/nsMsgSendReport.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMsgSendReport_h__ +#define __nsMsgSendReport_h__ + +#include "nsIMsgSendReport.h" +#include "nsString.h" +#include "nsCOMPtr.h" + +class nsMsgProcessReport : public nsIMsgProcessReport { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGPROCESSREPORT + + nsMsgProcessReport(); + + private: + virtual ~nsMsgProcessReport(); + bool mProceeded; + nsresult mError; + nsString mMessage; +}; + +class nsMsgSendReport : public nsIMsgSendReport { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSENDREPORT + + nsMsgSendReport(); + + protected: + virtual ~nsMsgSendReport(); + + private: +#define SEND_LAST_PROCESS process_FCC + nsCOMPtr<nsIMsgProcessReport> mProcessReport[SEND_LAST_PROCESS + 1]; + int32_t mDeliveryMode; + int32_t mCurrentProcess; + bool mAlreadyDisplayReport; +}; + +#endif diff --git a/comm/mailnews/compose/src/nsSmtpUrl.cpp b/comm/mailnews/compose/src/nsSmtpUrl.cpp new file mode 100644 index 0000000000..b9ac451650 --- /dev/null +++ b/comm/mailnews/compose/src/nsSmtpUrl.cpp @@ -0,0 +1,755 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" + +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsSmtpUrl.h" +#include "nsString.h" +#include "nsMsgUtils.h" +#include "nsIMimeConverter.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsCRT.h" +#include "mozilla/Encoding.h" + +///////////////////////////////////////////////////////////////////////////////////// +// mailto url definition +///////////////////////////////////////////////////////////////////////////////////// +nsMailtoUrl::nsMailtoUrl() { mFormat = nsIMsgCompFormat::Default; } + +nsMailtoUrl::~nsMailtoUrl() {} + +NS_IMPL_ISUPPORTS(nsMailtoUrl, nsIMailtoUrl, nsIURI) + +static void UnescapeAndConvert(nsIMimeConverter* mimeConverter, + const nsACString& escaped, nsACString& out) { + NS_ASSERTION(mimeConverter, "Set mimeConverter before calling!"); + // If the string is empty, do absolutely nothing. + if (escaped.IsEmpty()) return; + + MsgUnescapeString(escaped, 0, out); + nsAutoCString decodedString; + nsresult rv = mimeConverter->DecodeMimeHeaderToUTF8(out, "UTF_8", false, true, + decodedString); + if (NS_SUCCEEDED(rv) && !decodedString.IsEmpty()) out = decodedString; +} + +nsresult nsMailtoUrl::ParseMailtoUrl(char* searchPart) { + char* rest = searchPart; + nsCString escapedInReplyToPart; + nsCString escapedToPart; + nsCString escapedCcPart; + nsCString escapedSubjectPart; + nsCString escapedNewsgroupPart; + nsCString escapedNewsHostPart; + nsCString escapedReferencePart; + nsCString escapedBodyPart; + nsCString escapedBccPart; + nsCString escapedFollowUpToPart; + nsCString escapedFromPart; + nsCString escapedHtmlPart; + nsCString escapedOrganizationPart; + nsCString escapedReplyToPart; + nsCString escapedPriorityPart; + + // okay, first, free up all of our old search part state..... + CleanupMailtoState(); + // m_toPart has the escaped address from before the query string, copy it + // over so we can add on any additional to= addresses and unescape them all. + escapedToPart = m_toPart; + + if (rest && *rest == '?') { + /* start past the '?' */ + rest++; + } + + if (rest) { + char* token = NS_strtok("&", &rest); + while (token && *token) { + char* value = 0; + char* eq = PL_strchr(token, '='); + if (eq) { + value = eq + 1; + *eq = 0; + } + + nsCString decodedName; + MsgUnescapeString(nsDependentCString(token), 0, decodedName); + + if (decodedName.IsEmpty()) break; + + switch (NS_ToUpper(decodedName.First())) { + /* DO NOT support attachment= in mailto urls. This poses a security + fire hole!!! case 'A': if (!PL_strcasecmp (token, "attachment")) + m_attachmentPart = value; + break; + */ + case 'B': + if (decodedName.LowerCaseEqualsLiteral("bcc")) { + if (!escapedBccPart.IsEmpty()) { + escapedBccPart += ", "; + escapedBccPart += value; + } else + escapedBccPart = value; + } else if (decodedName.LowerCaseEqualsLiteral("body")) { + if (!escapedBodyPart.IsEmpty()) { + escapedBodyPart += "\n"; + escapedBodyPart += value; + } else + escapedBodyPart = value; + } + break; + case 'C': + if (decodedName.LowerCaseEqualsLiteral("cc")) { + if (!escapedCcPart.IsEmpty()) { + escapedCcPart += ", "; + escapedCcPart += value; + } else + escapedCcPart = value; + } + break; + case 'F': + if (decodedName.LowerCaseEqualsLiteral("followup-to")) + escapedFollowUpToPart = value; + else if (decodedName.LowerCaseEqualsLiteral("from")) + escapedFromPart = value; + break; + case 'H': + if (decodedName.LowerCaseEqualsLiteral("html-part") || + decodedName.LowerCaseEqualsLiteral("html-body")) { + // escapedHtmlPart holds the body for both html-part and html-body. + escapedHtmlPart = value; + mFormat = nsIMsgCompFormat::HTML; + } + break; + case 'I': + if (decodedName.LowerCaseEqualsLiteral("in-reply-to")) + escapedInReplyToPart = value; + break; + + case 'N': + if (decodedName.LowerCaseEqualsLiteral("newsgroups")) + escapedNewsgroupPart = value; + else if (decodedName.LowerCaseEqualsLiteral("newshost")) + escapedNewsHostPart = value; + break; + case 'O': + if (decodedName.LowerCaseEqualsLiteral("organization")) + escapedOrganizationPart = value; + break; + case 'R': + if (decodedName.LowerCaseEqualsLiteral("references")) + escapedReferencePart = value; + else if (decodedName.LowerCaseEqualsLiteral("reply-to")) + escapedReplyToPart = value; + break; + case 'S': + if (decodedName.LowerCaseEqualsLiteral("subject")) + escapedSubjectPart = value; + break; + case 'P': + if (decodedName.LowerCaseEqualsLiteral("priority")) + escapedPriorityPart = PL_strdup(value); + break; + case 'T': + if (decodedName.LowerCaseEqualsLiteral("to")) { + if (!escapedToPart.IsEmpty()) { + escapedToPart += ", "; + escapedToPart += value; + } else + escapedToPart = value; + } + break; + default: + break; + } // end of switch statement... + + if (eq) *eq = '='; /* put it back */ + token = NS_strtok("&", &rest); + } // while we still have part of the url to parse... + } // if rest && *rest + + nsresult rv; + // Get a global converter + nsCOMPtr<nsIMimeConverter> mimeConverter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Now unescape everything, and mime-decode the things that can be encoded. + UnescapeAndConvert(mimeConverter, escapedToPart, m_toPart); + UnescapeAndConvert(mimeConverter, escapedCcPart, m_ccPart); + UnescapeAndConvert(mimeConverter, escapedBccPart, m_bccPart); + UnescapeAndConvert(mimeConverter, escapedSubjectPart, m_subjectPart); + UnescapeAndConvert(mimeConverter, escapedNewsgroupPart, m_newsgroupPart); + UnescapeAndConvert(mimeConverter, escapedReferencePart, m_referencePart); + if (!escapedBodyPart.IsEmpty()) + MsgUnescapeString(escapedBodyPart, 0, m_bodyPart); + if (!escapedHtmlPart.IsEmpty()) + MsgUnescapeString(escapedHtmlPart, 0, m_htmlPart); + UnescapeAndConvert(mimeConverter, escapedNewsHostPart, m_newsHostPart); + UnescapeAndConvert(mimeConverter, escapedFollowUpToPart, m_followUpToPart); + UnescapeAndConvert(mimeConverter, escapedFromPart, m_fromPart); + UnescapeAndConvert(mimeConverter, escapedOrganizationPart, + m_organizationPart); + UnescapeAndConvert(mimeConverter, escapedReplyToPart, m_replyToPart); + UnescapeAndConvert(mimeConverter, escapedPriorityPart, m_priorityPart); + + nsCString inReplyToPart; // Not a member like the others... + UnescapeAndConvert(mimeConverter, escapedInReplyToPart, inReplyToPart); + + if (!inReplyToPart.IsEmpty()) { + // Ensure that References and In-Reply-To are consistent... The last + // reference will be used as In-Reply-To header. + if (m_referencePart.IsEmpty()) { + // If References is not set, set it to be the In-Reply-To. + m_referencePart = inReplyToPart; + } else { + // References is set. Add the In-Reply-To as last header unless it's + // set as last reference already. + int32_t lastRefStart = m_referencePart.RFindChar('<'); + nsAutoCString lastReference; + if (lastRefStart != -1) + lastReference = StringTail(m_referencePart, lastRefStart); + else + lastReference = m_referencePart; + + if (lastReference != inReplyToPart) { + m_referencePart += " "; + m_referencePart += inReplyToPart; + } + } + } + + return NS_OK; +} + +nsresult nsMailtoUrl::SetSpecInternal(const nsACString& aSpec) { + nsresult rv = NS_MutateURI(NS_SIMPLEURIMUTATOR_CONTRACTID) + .SetSpec(aSpec) + .Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +nsresult nsMailtoUrl::CleanupMailtoState() { + m_ccPart = ""; + m_subjectPart = ""; + m_newsgroupPart = ""; + m_newsHostPart = ""; + m_referencePart = ""; + m_bodyPart = ""; + m_bccPart = ""; + m_followUpToPart = ""; + m_fromPart = ""; + m_htmlPart = ""; + m_organizationPart = ""; + m_replyToPart = ""; + m_priorityPart = ""; + return NS_OK; +} + +nsresult nsMailtoUrl::ParseUrl() { + // we can get the path from the simple url..... + nsCString escapedPath; + m_baseURL->GetPathQueryRef(escapedPath); + + int32_t startOfSearchPart = escapedPath.FindChar('?'); + if (startOfSearchPart >= 0) { + // now parse out the search field... + nsAutoCString searchPart(Substring(escapedPath, startOfSearchPart)); + + if (!searchPart.IsEmpty()) { + // now we need to strip off the search part from the + // to part.... + escapedPath.SetLength(startOfSearchPart); + MsgUnescapeString(escapedPath, 0, m_toPart); + ParseMailtoUrl(searchPart.BeginWriting()); + } + } else if (!escapedPath.IsEmpty()) { + MsgUnescapeString(escapedPath, 0, m_toPart); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetMessageContents(nsACString& aToPart, nsACString& aCcPart, + nsACString& aBccPart, nsACString& aSubjectPart, + nsACString& aBodyPart, nsACString& aHtmlPart, + nsACString& aReferencePart, + nsACString& aNewsgroupPart, + MSG_ComposeFormat* aFormat) { + NS_ENSURE_ARG_POINTER(aFormat); + + aToPart = m_toPart; + aCcPart = m_ccPart; + aBccPart = m_bccPart; + aSubjectPart = m_subjectPart; + aBodyPart = m_bodyPart; + aHtmlPart = m_htmlPart; + aReferencePart = m_referencePart; + aNewsgroupPart = m_newsgroupPart; + *aFormat = mFormat; + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetFromPart(nsACString& aResult) { + aResult = m_fromPart; + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetFollowUpToPart(nsACString& aResult) { + aResult = m_followUpToPart; + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetOrganizationPart(nsACString& aResult) { + aResult = m_organizationPart; + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetReplyToPart(nsACString& aResult) { + aResult = m_replyToPart; + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetPriorityPart(nsACString& aResult) { + aResult = m_priorityPart; + return NS_OK; +} + +NS_IMETHODIMP +nsMailtoUrl::GetNewsHostPart(nsACString& aResult) { + aResult = m_newsHostPart; + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////// +// Begin nsIURI support +////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsMailtoUrl::GetSpec(nsACString& aSpec) { + return m_baseURL->GetSpec(aSpec); +} + +NS_IMETHODIMP nsMailtoUrl::GetPrePath(nsACString& aPrePath) { + return m_baseURL->GetPrePath(aPrePath); +} + +NS_IMETHODIMP nsMailtoUrl::GetScheme(nsACString& aScheme) { + return m_baseURL->GetScheme(aScheme); +} + +nsresult nsMailtoUrl::SetScheme(const nsACString& aScheme) { + nsresult rv = NS_MutateURI(m_baseURL).SetScheme(aScheme).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetUserPass(nsACString& aUserPass) { + return m_baseURL->GetUserPass(aUserPass); +} + +nsresult nsMailtoUrl::SetUserPass(const nsACString& aUserPass) { + nsresult rv = + NS_MutateURI(m_baseURL).SetUserPass(aUserPass).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetUsername(nsACString& aUsername) { + return m_baseURL->GetUsername(aUsername); +} + +nsresult nsMailtoUrl::SetUsername(const nsACString& aUsername) { + nsresult rv = + NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetPassword(nsACString& aPassword) { + return m_baseURL->GetPassword(aPassword); +} + +nsresult nsMailtoUrl::SetPassword(const nsACString& aPassword) { + nsresult rv = + NS_MutateURI(m_baseURL).SetPassword(aPassword).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetHostPort(nsACString& aHostPort) { + return m_baseURL->GetHost(aHostPort); +} + +nsresult nsMailtoUrl::SetHostPort(const nsACString& aHostPort) { + nsresult rv = + NS_MutateURI(m_baseURL).SetHostPort(aHostPort).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetHost(nsACString& aHost) { + return m_baseURL->GetHost(aHost); +} + +nsresult nsMailtoUrl::SetHost(const nsACString& aHost) { + nsresult rv = NS_MutateURI(m_baseURL).SetHost(aHost).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetPort(int32_t* aPort) { + return m_baseURL->GetPort(aPort); +} + +nsresult nsMailtoUrl::SetPort(int32_t aPort) { + nsresult rv = NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetPathQueryRef(nsACString& aPath) { + return m_baseURL->GetPathQueryRef(aPath); +} + +nsresult nsMailtoUrl::SetPathQueryRef(const nsACString& aPath) { + nsresult rv = + NS_MutateURI(m_baseURL).SetPathQueryRef(aPath).Finalize(m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + return ParseUrl(); +} + +NS_IMETHODIMP nsMailtoUrl::GetAsciiHost(nsACString& aHostA) { + return m_baseURL->GetAsciiHost(aHostA); +} + +NS_IMETHODIMP nsMailtoUrl::GetAsciiHostPort(nsACString& aHostPortA) { + return m_baseURL->GetAsciiHostPort(aHostPortA); +} + +NS_IMETHODIMP nsMailtoUrl::GetAsciiSpec(nsACString& aSpecA) { + return m_baseURL->GetAsciiSpec(aSpecA); +} + +NS_IMETHODIMP nsMailtoUrl::SchemeIs(const char* aScheme, bool* _retval) { + return m_baseURL->SchemeIs(aScheme, _retval); +} + +NS_IMETHODIMP nsMailtoUrl::Equals(nsIURI* other, bool* _retval) { + // The passed-in URI might be an nsMailtoUrl. Pass our inner URL to its + // Equals method. The other nsMailtoUrl will then pass its inner URL to + // to the Equals method of our inner URL. Other URIs will return false. + if (other) return other->Equals(m_baseURL, _retval); + + return m_baseURL->Equals(other, _retval); +} + +nsresult nsMailtoUrl::Clone(nsIURI** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + RefPtr<nsMailtoUrl> clone = new nsMailtoUrl(); + + NS_ENSURE_TRUE(clone, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = NS_MutateURI(m_baseURL).Finalize(clone->m_baseURL); + NS_ENSURE_SUCCESS(rv, rv); + clone->ParseUrl(); + clone.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP nsMailtoUrl::Resolve(const nsACString& relativePath, + nsACString& result) { + return m_baseURL->Resolve(relativePath, result); +} + +nsresult nsMailtoUrl::SetRef(const nsACString& aRef) { + return NS_MutateURI(m_baseURL).SetRef(aRef).Finalize(m_baseURL); +} + +NS_IMETHODIMP +nsMailtoUrl::GetRef(nsACString& result) { return m_baseURL->GetRef(result); } + +NS_IMETHODIMP nsMailtoUrl::EqualsExceptRef(nsIURI* other, bool* result) { + // The passed-in URI might be an nsMailtoUrl. Pass our inner URL to its + // Equals method. The other nsMailtoUrl will then pass its inner URL to + // to the Equals method of our inner URL. Other URIs will return false. + if (other) return other->EqualsExceptRef(m_baseURL, result); + + return m_baseURL->EqualsExceptRef(other, result); +} + +NS_IMETHODIMP +nsMailtoUrl::GetSpecIgnoringRef(nsACString& result) { + return m_baseURL->GetSpecIgnoringRef(result); +} + +NS_IMETHODIMP +nsMailtoUrl::GetDisplaySpec(nsACString& aUnicodeSpec) { + return m_baseURL->GetDisplaySpec(aUnicodeSpec); +} + +NS_IMETHODIMP +nsMailtoUrl::GetDisplayHostPort(nsACString& aUnicodeHostPort) { + return m_baseURL->GetDisplayHostPort(aUnicodeHostPort); +} + +NS_IMETHODIMP +nsMailtoUrl::GetDisplayHost(nsACString& aUnicodeHost) { + return m_baseURL->GetDisplayHost(aUnicodeHost); +} + +NS_IMETHODIMP +nsMailtoUrl::GetDisplayPrePath(nsACString& aPrePath) { + return m_baseURL->GetDisplayPrePath(aPrePath); +} + +NS_IMETHODIMP +nsMailtoUrl::GetHasRef(bool* result) { return m_baseURL->GetHasRef(result); } + +NS_IMETHODIMP +nsMailtoUrl::GetFilePath(nsACString& aFilePath) { + return m_baseURL->GetFilePath(aFilePath); +} + +nsresult nsMailtoUrl::SetFilePath(const nsACString& aFilePath) { + return NS_MutateURI(m_baseURL).SetFilePath(aFilePath).Finalize(m_baseURL); +} + +NS_IMETHODIMP +nsMailtoUrl::GetQuery(nsACString& aQuery) { + return m_baseURL->GetQuery(aQuery); +} + +nsresult nsMailtoUrl::SetQuery(const nsACString& aQuery) { + return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL); +} + +nsresult nsMailtoUrl::SetQueryWithEncoding(const nsACString& aQuery, + const mozilla::Encoding* aEncoding) { + return NS_MutateURI(m_baseURL) + .SetQueryWithEncoding(aQuery, aEncoding) + .Finalize(m_baseURL); +} + +NS_IMETHODIMP_(void) +nsMailtoUrl::Serialize(mozilla::ipc::URIParams& aParams) { + m_baseURL->Serialize(aParams); +} + +NS_IMPL_ISUPPORTS(nsMailtoUrl::Mutator, nsIURISetters, nsIURIMutator) + +NS_IMETHODIMP +nsMailtoUrl::Mutate(nsIURIMutator** aMutator) { + RefPtr<nsMailtoUrl::Mutator> mutator = new nsMailtoUrl::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} + +nsresult nsMailtoUrl::NewMailtoURI(const nsACString& aSpec, nsIURI* aBaseURI, + nsIURI** _retval) { + nsresult rv; + + nsCOMPtr<nsIURI> mailtoUrl; + rv = NS_MutateURI(new Mutator()).SetSpec(aSpec).Finalize(mailtoUrl); + NS_ENSURE_SUCCESS(rv, rv); + + mailtoUrl.forget(_retval); + return NS_OK; +} + +///////////////////////////////////////////////////////////////////////////////////// +// smtp url definition +///////////////////////////////////////////////////////////////////////////////////// + +nsSmtpUrl::nsSmtpUrl() : nsMsgMailNewsUrl() { + // nsISmtpUrl specific state... + + m_isPostMessage = true; + m_requestDSN = false; + m_verifyLogon = false; +} + +nsSmtpUrl::~nsSmtpUrl() {} + +NS_IMPL_ISUPPORTS_INHERITED(nsSmtpUrl, nsMsgMailNewsUrl, nsISmtpUrl) + +//////////////////////////////////////////////////////////////////////////////////// +// Begin nsISmtpUrl specific support + +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsSmtpUrl::SetRecipients(const char* aRecipientsList) { + NS_ENSURE_ARG(aRecipientsList); + MsgUnescapeString(nsDependentCString(aRecipientsList), 0, m_toPart); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetRecipients(char** aRecipientsList) { + NS_ENSURE_ARG_POINTER(aRecipientsList); + if (aRecipientsList) *aRecipientsList = ToNewCString(m_toPart); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::SetSender(const char* aSender) { + NS_ENSURE_ARG(aSender); + MsgUnescapeString(nsDependentCString(aSender), 0, m_fromPart); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetSender(char** aSender) { + NS_ENSURE_ARG_POINTER(aSender); + if (aSender) *aSender = ToNewCString(m_fromPart); + return NS_OK; +} + +NS_IMPL_GETSET(nsSmtpUrl, PostMessage, bool, m_isPostMessage) + +NS_IMPL_GETSET(nsSmtpUrl, VerifyLogon, bool, m_verifyLogon) + +// the message can be stored in a file....allow accessors for getting and +// setting the file name to post... +NS_IMETHODIMP nsSmtpUrl::SetPostMessageFile(nsIFile* aFile) { + NS_ENSURE_ARG_POINTER(aFile); + m_fileName = aFile; + return NS_OK; +} + +NS_IMETHODIMP nsSmtpUrl::GetPostMessageFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + if (m_fileName) { + // Clone the file so nsLocalFile stat caching doesn't make the caller get + // the wrong file size. + m_fileName->Clone(aFile); + return *aFile ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMPL_GETSET(nsSmtpUrl, RequestDSN, bool, m_requestDSN) + +NS_IMETHODIMP +nsSmtpUrl::SetDsnEnvid(const nsACString& aDsnEnvid) { + m_dsnEnvid = aDsnEnvid; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetDsnEnvid(nsACString& aDsnEnvid) { + aDsnEnvid = m_dsnEnvid; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetSenderIdentity(nsIMsgIdentity** aSenderIdentity) { + NS_ENSURE_ARG_POINTER(aSenderIdentity); + NS_ADDREF(*aSenderIdentity = m_senderIdentity); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::SetSenderIdentity(nsIMsgIdentity* aSenderIdentity) { + NS_ENSURE_ARG_POINTER(aSenderIdentity); + m_senderIdentity = aSenderIdentity; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::SetPrompt(nsIPrompt* aNetPrompt) { + NS_ENSURE_ARG_POINTER(aNetPrompt); + m_netPrompt = aNetPrompt; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetPrompt(nsIPrompt** aNetPrompt) { + NS_ENSURE_ARG_POINTER(aNetPrompt); + NS_ENSURE_TRUE(m_netPrompt, NS_ERROR_NULL_POINTER); + NS_ADDREF(*aNetPrompt = m_netPrompt); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::SetAuthPrompt(nsIAuthPrompt* aNetAuthPrompt) { + NS_ENSURE_ARG_POINTER(aNetAuthPrompt); + m_netAuthPrompt = aNetAuthPrompt; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetAuthPrompt(nsIAuthPrompt** aNetAuthPrompt) { + NS_ENSURE_ARG_POINTER(aNetAuthPrompt); + NS_ENSURE_TRUE(m_netAuthPrompt, NS_ERROR_NULL_POINTER); + NS_ADDREF(*aNetAuthPrompt = m_netAuthPrompt); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { + NS_ENSURE_ARG_POINTER(aCallbacks); + m_callbacks = aCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { + NS_ENSURE_ARG_POINTER(aCallbacks); + NS_ENSURE_TRUE(m_callbacks, NS_ERROR_NULL_POINTER); + NS_ADDREF(*aCallbacks = m_callbacks); + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::SetSmtpServer(nsISmtpServer* aSmtpServer) { + NS_ENSURE_ARG_POINTER(aSmtpServer); + m_smtpServer = aSmtpServer; + return NS_OK; +} + +NS_IMETHODIMP +nsSmtpUrl::GetSmtpServer(nsISmtpServer** aSmtpServer) { + NS_ENSURE_ARG_POINTER(aSmtpServer); + NS_ENSURE_TRUE(m_smtpServer, NS_ERROR_NULL_POINTER); + NS_ADDREF(*aSmtpServer = m_smtpServer); + return NS_OK; +} + +nsresult nsSmtpUrl::NewSmtpURI(const nsACString& aSpec, nsIURI* aBaseURI, + nsIURI** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = 0; + nsresult rv; + nsCOMPtr<nsIMsgMailNewsUrl> aSmtpUri = new nsSmtpUrl(); + if (aBaseURI) { + nsAutoCString newSpec; + rv = aBaseURI->Resolve(aSpec, newSpec); + NS_ENSURE_SUCCESS(rv, rv); + rv = aSmtpUri->SetSpecInternal(newSpec); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = aSmtpUri->SetSpecInternal(aSpec); + NS_ENSURE_SUCCESS(rv, rv); + } + aSmtpUri.forget(_retval); + + return rv; +} diff --git a/comm/mailnews/compose/src/nsSmtpUrl.h b/comm/mailnews/compose/src/nsSmtpUrl.h new file mode 100644 index 0000000000..bd205f119c --- /dev/null +++ b/comm/mailnews/compose/src/nsSmtpUrl.h @@ -0,0 +1,144 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSmtpUrl_h__ +#define nsSmtpUrl_h__ + +#include "nsISmtpUrl.h" +#include "nsIURI.h" +#include "nsMsgMailNewsUrl.h" +#include "nsIMsgIdentity.h" +#include "nsCOMPtr.h" +#include "nsIPrompt.h" +#include "nsIAuthPrompt.h" +#include "nsISmtpServer.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIURIMutator.h" + +class nsMailtoUrl : public nsIMailtoUrl, public nsIURI { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIURI + NS_DECL_NSIMAILTOURL + + nsMailtoUrl(); + static nsresult NewMailtoURI(const nsACString& aSpec, nsIURI* aBaseURI, + nsIURI** _retval); + + protected: + virtual nsresult Clone(nsIURI** _retval); + virtual nsresult SetSpecInternal(const nsACString& aSpec); + virtual nsresult SetScheme(const nsACString& aScheme); + virtual nsresult SetUserPass(const nsACString& aUserPass); + virtual nsresult SetUsername(const nsACString& aUsername); + virtual nsresult SetPassword(const nsACString& aPassword); + virtual nsresult SetHostPort(const nsACString& aHostPort); + virtual nsresult SetHost(const nsACString& aHost); + virtual nsresult SetPort(int32_t aPort); + virtual nsresult SetPathQueryRef(const nsACString& aPath); + virtual nsresult SetRef(const nsACString& aRef); + virtual nsresult SetFilePath(const nsACString& aFilePath); + virtual nsresult SetQuery(const nsACString& aQuery); + virtual nsresult SetQueryWithEncoding(const nsACString& aQuery, + const mozilla::Encoding* aEncoding); + + public: + class Mutator : public nsIURIMutator, public BaseURIMutator<nsMailtoUrl> { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + + NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD Finalize(nsIURI** aURI) override { + mURI.forget(aURI); + return NS_OK; + } + + NS_IMETHOD SetSpec(const nsACString& aSpec, + nsIURIMutator** aMutator) override { + if (aMutator) NS_ADDREF(*aMutator = this); + return InitFromSpec(aSpec); + } + + explicit Mutator() {} + + private: + virtual ~Mutator() {} + + friend class nsMailtoUrl; + }; + friend BaseURIMutator<nsMailtoUrl>; + + protected: + virtual ~nsMailtoUrl(); + nsresult ParseUrl(); + nsresult CleanupMailtoState(); + nsresult ParseMailtoUrl(char* searchPart); + + nsCOMPtr<nsIURI> m_baseURL; + + // data retrieved from parsing the url: (Note the url could be a post from + // file or it could be in the url) + nsCString m_toPart; + nsCString m_ccPart; + nsCString m_subjectPart; + nsCString m_newsgroupPart; + nsCString m_newsHostPart; + nsCString m_referencePart; + nsCString m_bodyPart; + nsCString m_bccPart; + nsCString m_followUpToPart; + nsCString m_fromPart; + nsCString m_htmlPart; + nsCString m_organizationPart; + nsCString m_replyToPart; + nsCString m_priorityPart; + + MSG_ComposeFormat mFormat; +}; + +class nsSmtpUrl : public nsISmtpUrl, public nsMsgMailNewsUrl { + public: + NS_DECL_ISUPPORTS_INHERITED + + // From nsISmtpUrl + NS_DECL_NSISMTPURL + + // nsSmtpUrl + nsSmtpUrl(); + static nsresult NewSmtpURI(const nsACString& aSpec, nsIURI* aBaseURI, + nsIURI** _retval); + + protected: + virtual ~nsSmtpUrl(); + + // data retrieved from parsing the url: (Note the url could be a post from + // file or it could be in the url) + nsCString m_toPart; + nsCString m_fromPart; + + bool m_isPostMessage; + bool m_requestDSN; + nsCString m_dsnEnvid; + bool m_verifyLogon; + + // Smtp specific event sinks + nsCOMPtr<nsIFile> m_fileName; + nsCOMPtr<nsIMsgIdentity> m_senderIdentity; + nsCOMPtr<nsIPrompt> m_netPrompt; + nsCOMPtr<nsIAuthPrompt> m_netAuthPrompt; + nsCOMPtr<nsIInterfaceRequestor> m_callbacks; + nsCOMPtr<nsISmtpServer> m_smtpServer; + + // it is possible to encode the message to parse in the form of a url. + // This function is used to decompose the search and path part into the bare + // message components (to, fcc, bcc, etc.) + nsresult ParseMessageToPost(char* searchPart); +}; + +#endif // nsSmtpUrl_h__ diff --git a/comm/mailnews/compose/test/moz.build b/comm/mailnews/compose/test/moz.build new file mode 100644 index 0000000000..512bee25fc --- /dev/null +++ b/comm/mailnews/compose/test/moz.build @@ -0,0 +1,8 @@ +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += [ + "unit/xpcshell.ini", +] diff --git a/comm/mailnews/compose/test/unit/data/429891_testcase.eml b/comm/mailnews/compose/test/unit/data/429891_testcase.eml new file mode 100644 index 0000000000..b4fb4164c9 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/429891_testcase.eml @@ -0,0 +1,384 @@ +From: Invalid User <from_A@foo.invalid>
+To: =?UTF-8?B?RnLDqcOpZGxlLCBUZXN0?= <to_A@foo.invalid>
+Subject: Big email
+
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
+012345678901234567890123456789012345678901234567890123456789
diff --git a/comm/mailnews/compose/test/unit/data/binary-after-plain.txt b/comm/mailnews/compose/test/unit/data/binary-after-plain.txt Binary files differnew file mode 100644 index 0000000000..cec0697428 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/binary-after-plain.txt diff --git a/comm/mailnews/compose/test/unit/data/listexpansion.sql b/comm/mailnews/compose/test/unit/data/listexpansion.sql new file mode 100644 index 0000000000..6c4f6491be --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/listexpansion.sql @@ -0,0 +1,126 @@ +-- Address book with nested mailing lists for use in test_expandMailingLists.js. +PRAGMA user_version = 1; + +CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER); +CREATE TABLE properties (card TEXT, name TEXT, value TEXT); +CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT); +CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card)); + +INSERT INTO cards (uid, localId) VALUES + ('813155c6-924d-4751-95d0-70d8e64f16bc', 1), -- homer + ('b2cc8395-d959-45e4-9516-17457adb16fa', 2), -- marge + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 3), -- bart + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 4), -- lisa + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 5), -- maggie + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 6), --simpson + ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 7), --marge + ('ad305609-3535-4d51-8c96-cd82d93aed46', 8), --family + ('4808121d-ebad-4564-864d-8f1149aa053b', 9), --kids + ('4926ff7a-e929-475a-8aa8-2baac994390c', 10), --parents + ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 11), --older-kids + ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 12), --bad-kids + ('34e60324-4fb6-4f10-ab1b-333b07680228', 13); --bad-younger-kids + +INSERT INTO properties (card, name, value) VALUES + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PrimaryEmail', 'homer@example.com'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PhotoType', 'generic'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'LowercasePrimaryEmail', 'homer@example.com'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'DisplayName', 'Simpson'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'LastModifiedDate', '1473722922'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PopularityIndex', '0'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PreferMailFormat', '0'), + ('813155c6-924d-4751-95d0-70d8e64f16bc', 'PreferDisplayName', '1'), + + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'DisplayName', 'Marge'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PrimaryEmail', 'marge@example.com'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PhotoType', 'generic'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'LowercasePrimaryEmail', 'marge@example.com'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'LastModifiedDate', '1473723020'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PopularityIndex', '0'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PreferMailFormat', '0'), + ('b2cc8395-d959-45e4-9516-17457adb16fa', 'PreferDisplayName', '1'), + + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PhotoType', 'generic'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PopularityIndex', '0'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PreferMailFormat', '0'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PreferDisplayName', '1'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'DisplayName', 'Bart'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'PrimaryEmail', 'bart@foobar.invalid'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'LowercasePrimaryEmail', 'bart@foobar.invalid'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'SecondEmail', 'bart@example.com'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'LowercaseSecondEmail', 'bart@example.com'), + ('979f194e-49f2-4bbb-b364-598cdc6a7d11', 'LastModifiedDate', '1473716192'), + + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PrimaryEmail', 'lisa@example.com'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PhotoType', 'generic'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'LowercasePrimaryEmail', 'lisa@example.com'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'DisplayName', 'lisa@example.com'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PopularityIndex', '0'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PreferMailFormat', '0'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'LastModifiedDate', '0'), + ('4dd13a79-b70c-4b43-bdba-bacd4e977c1b', 'PreferDisplayName', '1'), + + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'DisplayName', 'Maggie'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'LastModifiedDate', '1473723047'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PrimaryEmail', 'maggie@example.com'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PhotoType', 'generic'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'LowercasePrimaryEmail', 'maggie@example.com'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PopularityIndex', '0'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PreferMailFormat', '0'), + ('c96402d7-1c7b-4242-a35c-b92c8ec9dfa2', 'PreferDisplayName', '1'), + + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 'DisplayName', 'simpson'), + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 'PrimaryEmail', 'simpson'), + ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 'DisplayName', 'marge'), + ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 'PrimaryEmail', 'marge'), + ('ad305609-3535-4d51-8c96-cd82d93aed46', 'DisplayName', 'family'), + ('ad305609-3535-4d51-8c96-cd82d93aed46', 'PrimaryEmail', 'family'), + ('4808121d-ebad-4564-864d-8f1149aa053b', 'DisplayName', 'kids'), + ('4808121d-ebad-4564-864d-8f1149aa053b', 'PrimaryEmail', 'kids'), + ('4926ff7a-e929-475a-8aa8-2baac994390c', 'DisplayName', 'parents'), + ('4926ff7a-e929-475a-8aa8-2baac994390c', 'PrimaryEmail', 'parents'), + ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 'PrimaryEmail', 'older-kids'), + ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 'DisplayName', 'older-kids'), + ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 'DisplayName', 'bad-kids'), + ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 'PrimaryEmail', 'bad-kids'), + ('34e60324-4fb6-4f10-ab1b-333b07680228', 'DisplayName', 'bad-younger-kids'), + ('34e60324-4fb6-4f10-ab1b-333b07680228', 'PrimaryEmail', 'bad-younger-kids'); + +INSERT INTO lists (uid, localId, name, nickName, description) VALUES + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 1, 'simpson', '', ''), + ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 2, 'marge', '', 'marges own list'), + ('ad305609-3535-4d51-8c96-cd82d93aed46', 3, 'family', '', ''), + ('4808121d-ebad-4564-864d-8f1149aa053b', 4, 'kids', '', ''), + ('4926ff7a-e929-475a-8aa8-2baac994390c', 5, 'parents', '', ''), + ('84fa4513-9b60-4379-ade7-1e4b48d67c84', 6, 'older-kids', '', ''), + ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', 7, 'bad-kids', '', ''), + ('34e60324-4fb6-4f10-ab1b-333b07680228', 8, 'bad-younger-kids', '', ''); + +INSERT INTO list_cards (list, card) VALUES + -- simpson + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', '813155c6-924d-4751-95d0-70d8e64f16bc'), -- homer + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', 'b2cc8395-d959-45e4-9516-17457adb16fa'), -- marge + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', '979f194e-49f2-4bbb-b364-598cdc6a7d11'), -- bart + ('5ec12f1d-7ee9-403c-a617-48596dacbc18', '4dd13a79-b70c-4b43-bdba-bacd4e977c1b'), -- lisa + -- marge + ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', '813155c6-924d-4751-95d0-70d8e64f16bc'), -- homer + ('18204ef9-e4e3-4cd5-9981-604c69bbb9ee', 'b2cc8395-d959-45e4-9516-17457adb16fa'), -- marge + -- family + ('ad305609-3535-4d51-8c96-cd82d93aed46', '4926ff7a-e929-475a-8aa8-2baac994390c'), -- parents + ('ad305609-3535-4d51-8c96-cd82d93aed46', '4808121d-ebad-4564-864d-8f1149aa053b'), -- kids + -- parents + ('4926ff7a-e929-475a-8aa8-2baac994390c', '813155c6-924d-4751-95d0-70d8e64f16bc'), -- homer + ('4926ff7a-e929-475a-8aa8-2baac994390c', 'b2cc8395-d959-45e4-9516-17457adb16fa'), -- marge + ('4926ff7a-e929-475a-8aa8-2baac994390c', '4926ff7a-e929-475a-8aa8-2baac994390c'), -- parents + -- kids + ('4808121d-ebad-4564-864d-8f1149aa053b', '84fa4513-9b60-4379-ade7-1e4b48d67c84'), -- older-kids + ('4808121d-ebad-4564-864d-8f1149aa053b', 'c96402d7-1c7b-4242-a35c-b92c8ec9dfa2'), -- maggie + -- older-kids + ('84fa4513-9b60-4379-ade7-1e4b48d67c84', '4dd13a79-b70c-4b43-bdba-bacd4e977c1b'), -- lisa + ('84fa4513-9b60-4379-ade7-1e4b48d67c84', '979f194e-49f2-4bbb-b364-598cdc6a7d11'), -- bart + -- bad-kids + ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', '84fa4513-9b60-4379-ade7-1e4b48d67c84'), -- older-kids + ('8e88b9a4-2500-48e0-bcea-b1fa4eab6b72', '34e60324-4fb6-4f10-ab1b-333b07680228'), -- bad-younger-kids + -- bad-younger-kids + ('34e60324-4fb6-4f10-ab1b-333b07680228', 'c96402d7-1c7b-4242-a35c-b92c8ec9dfa2'), -- maggie + ('34e60324-4fb6-4f10-ab1b-333b07680228', '8e88b9a4-2500-48e0-bcea-b1fa4eab6b72'); -- bad-kids diff --git a/comm/mailnews/compose/test/unit/data/message1.eml b/comm/mailnews/compose/test/unit/data/message1.eml new file mode 100644 index 0000000000..7913f5f262 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/message1.eml @@ -0,0 +1,7 @@ +From: from_B@foo.invalid
+To: to_B@foo.invalid
+Subject: test mail
+
+this email is in dos format because that is what the interface requires
+
+test message
diff --git a/comm/mailnews/compose/test/unit/data/shift-jis.eml b/comm/mailnews/compose/test/unit/data/shift-jis.eml new file mode 100644 index 0000000000..58f583907d --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/shift-jis.eml @@ -0,0 +1,13 @@ +To: test@example.com +From: test@example.com +Subject: ISO-2022-JP and 7bit containing =67 and hence looking like quoted-printable +Message-ID: <10a2aa17-e92f-417c-864e-575d4e371702@example.com> +Date: Tue, 3 Apr 2018 19:09:16 +0900 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 + Thunderbird/52.6.0 +MIME-Version: 1.0 +Content-Type: text/plain; charset=SHIFT-JIS; format=flowed +Content-Language: ja-JP +Content-Transfer-Encoding: 7bit + + diff --git a/comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt b/comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt new file mode 100644 index 0000000000..cd370be3f8 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-ISO-2022-JP.txt @@ -0,0 +1 @@ +$B%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H%F%9%H(B
diff --git a/comm/mailnews/compose/test/unit/data/test-KOI8-R.txt b/comm/mailnews/compose/test/unit/data/test-KOI8-R.txt new file mode 100644 index 0000000000..91f77cae45 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-KOI8-R.txt @@ -0,0 +1,2 @@ + , , , ,
+ .
diff --git a/comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt b/comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt new file mode 100644 index 0000000000..7a7f267540 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-SHIFT_JIS.txt @@ -0,0 +1 @@ +Shift_JIS̃eLXgt@CłB
diff --git a/comm/mailnews/compose/test/unit/data/test-UTF-16BE.txt b/comm/mailnews/compose/test/unit/data/test-UTF-16BE.txt Binary files differnew file mode 100644 index 0000000000..dd5fd39ed2 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-UTF-16BE.txt diff --git a/comm/mailnews/compose/test/unit/data/test-UTF-16LE.txt b/comm/mailnews/compose/test/unit/data/test-UTF-16LE.txt Binary files differnew file mode 100644 index 0000000000..a13a8f09e1 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-UTF-16LE.txt diff --git a/comm/mailnews/compose/test/unit/data/test-UTF-8.txt b/comm/mailnews/compose/test/unit/data/test-UTF-8.txt new file mode 100644 index 0000000000..b5e9df9a45 --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-UTF-8.txt @@ -0,0 +1 @@ +测试文件 diff --git a/comm/mailnews/compose/test/unit/data/test-windows-1252.txt b/comm/mailnews/compose/test/unit/data/test-windows-1252.txt new file mode 100644 index 0000000000..a98046517a --- /dev/null +++ b/comm/mailnews/compose/test/unit/data/test-windows-1252.txt @@ -0,0 +1,2 @@ +Buenos das - Franois a t Paris - Budapester Strae, Berlin.
+This is text in windows-1252.
diff --git a/comm/mailnews/compose/test/unit/head_compose.js b/comm/mailnews/compose/test/unit/head_compose.js new file mode 100644 index 0000000000..6f874335e5 --- /dev/null +++ b/comm/mailnews/compose/test/unit/head_compose.js @@ -0,0 +1,280 @@ +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); +var { localAccountUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/LocalAccountUtils.jsm" +); + +var CC = Components.Constructor; + +// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo. +var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo(); + +// Ensure the profile directory is set up +do_get_profile(); + +var gDEPTH = "../../../../"; + +// Import the required setup scripts. + +/* import-globals-from ../../../test/resources/abSetup.js */ +load("../../../resources/abSetup.js"); + +// Import the smtp server scripts +var { + nsMailServer, + gThreadManager, + fsDebugNone, + fsDebugAll, + fsDebugRecv, + fsDebugRecvSend, +} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm"); +var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import( + "resource://testing-common/mailnews/Smtpd.jsm" +); +var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import( + "resource://testing-common/mailnews/Auth.jsm" +); + +var gDraftFolder; + +// Setup the daemon and server +function setupServerDaemon(handler) { + if (!handler) { + handler = function (d) { + return new SMTP_RFC2821_handler(d); + }; + } + var server = new nsMailServer(handler, new SmtpDaemon()); + return server; +} + +function getBasicSmtpServer(port = 1, hostname = "localhost") { + let server = localAccountUtils.create_outgoing_server( + port, + "user", + "password", + hostname + ); + + // Override the default greeting so we get something predicitable + // in the ELHO message + Services.prefs.setCharPref("mail.smtpserver.default.hello_argument", "test"); + + return server; +} + +function getSmtpIdentity(senderName, smtpServer) { + // Set up the identity + let identity = MailServices.accounts.createIdentity(); + identity.email = senderName; + identity.smtpServerKey = smtpServer.key; + + return identity; +} + +var test; + +function do_check_transaction(real, expected) { + if (Array.isArray(real)) { + real = real.at(-1); + } + // real.them may have an extra QUIT on the end, where the stream is only + // closed after we have a chance to process it and not them. We therefore + // excise this from the list + if (real.them[real.them.length - 1] == "QUIT") { + real.them.pop(); + } + + Assert.equal(real.them.join(","), expected.join(",")); + dump("Passed test " + test + "\n"); +} + +// This listener is designed just to call OnStopCopy() when its OnStopCopy +// function is called - the rest of the functions are unneeded for a lot of +// tests (but we can't use asyncCopyListener because we need the +// nsIMsgSendListener interface as well). +var copyListener = { + // nsIMsgSendListener + onStartSending(aMsgID, aMsgSize) {}, + onProgress(aMsgID, aProgress, aProgressMax) {}, + onStatus(aMsgID, aMsg) {}, + onStopSending(aMsgID, aStatus, aMsg, aReturnFile) {}, + onGetDraftFolderURI(aMsgID, aFolderURI) {}, + onSendNotPerformed(aMsgID, aStatus) {}, + onTransportSecurityError(msgID, status, secInfo, location) {}, + + // nsIMsgCopyServiceListener + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + GetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + /* globals OnStopCopy */ + OnStopCopy(aStatus); + }, + + // QueryInterface + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgSendListener", + "nsIMsgCopyServiceListener", + ]), +}; + +var progressListener = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this.resolve(gDraftFolder && mailTestUtils.firstMsgHdr(gDraftFolder)); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) {}, + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange(aWebProgress, aRequest, state) {}, + onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +function createMessage(aAttachment) { + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.from = "Nobody <nobody@tinderbox.test>"; + + let attachments = []; + if (aAttachment) { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + if (aAttachment instanceof Ci.nsIFile) { + attachment.url = "file://" + aAttachment.path; + attachment.contentType = "text/plain"; + attachment.name = aAttachment.leafName; + } else { + attachment.url = "data:,sometext"; + attachment.name = aAttachment; + } + attachments = [attachment]; + } + return richCreateMessage(fields, attachments); +} + +function richCreateMessage( + fields, + attachments = [], + identity = null, + account = null +) { + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + + let msgCompose = MailServices.compose.initCompose(params); + if (identity === null) { + identity = getSmtpIdentity(null, getBasicSmtpServer()); + } + + let rootFolder = localAccountUtils.rootFolder; + gDraftFolder = null; + // Make sure the drafts folder is empty + try { + gDraftFolder = rootFolder.getChildNamed("Drafts"); + } catch (e) { + // we don't have to remove the folder because it doesn't exist yet + gDraftFolder = rootFolder.createLocalSubfolder("Drafts"); + } + // Clear all messages + let msgs = [...gDraftFolder.msgDatabase.enumerateMessages()]; + if (msgs.length > 0) { + gDraftFolder.deleteMessages(msgs, null, true, false, null, false); + } + + // Set attachment + fields.removeAttachments(); + for (let attachment of attachments) { + fields.addAttachment(attachment); + } + + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgSaveAsDraft, + identity, + account ? account.key : "", + null, + progress + ); + return promise; +} + +function getAttachmentFromContent(aContent) { + function getBoundaryStringFromContent() { + let found = aContent.match( + /Content-Type: multipart\/mixed;\s+boundary="(.*?)"/ + ); + Assert.notEqual(found, null); + Assert.equal(found.length, 2); + + return found[1]; + } + + let boundary = getBoundaryStringFromContent(aContent); + let regex = new RegExp( + "\\r\\n\\r\\n--" + + boundary + + "\\r\\n" + + "([\\s\\S]*?)\\r\\n" + + "--" + + boundary + + "--", + "m" + ); + let attachments = aContent.match(regex); + Assert.notEqual(attachments, null); + Assert.equal(attachments.length, 2); + return attachments[1]; +} + +/** + * Get the body part of an MIME message. + * + * @param {string} content - The message content. + * @returns {string} + */ +function getMessageBody(content) { + let separatorIndex = content.indexOf("\r\n\r\n"); + Assert.equal(content.slice(-2), "\r\n", "Should end with a line break."); + return content.slice(separatorIndex + 4, -2); +} + +registerCleanupFunction(function () { + load(gDEPTH + "mailnews/resources/mailShutdown.js"); +}); diff --git a/comm/mailnews/compose/test/unit/test_accountKey.js b/comm/mailnews/compose/test/unit/test_accountKey.js new file mode 100644 index 0000000000..440b2ea78a --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_accountKey.js @@ -0,0 +1,80 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +let MockNntpService = { + QueryInterface: ChromeUtils.generateQI(["nsINntpService"]), + postMessage(messageFile, groupNames, accountKey, urlListener, msgWindow) { + this.messageFile = messageFile; + this.groupNames = groupNames; + this.accountKey = accountKey; + }, +}; + +let MockNntpServiceFactory = { + createInstance(aIID) { + return MockNntpService; + }, +}; + +add_setup(async function () { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + Components.ID("{4816dd44-fe15-4719-8cfb-a2f8ee46d787}"), + "Mock NntpService", + "@mozilla.org/messenger/nntpservice;1", + MockNntpServiceFactory + ); +}); + +/** + * Test that when accountKey is not passed to sendMessageFile, MessageSend can + * get the right account key from identity. + */ +add_task(async function testAccountKey() { + // Set up the servers. + let server = setupServerDaemon(); + localAccountUtils.loadLocalMailAccount(); + server.start(); + let smtpServer = getBasicSmtpServer(server.port); + let identity = getSmtpIdentity("from@foo.invalid", smtpServer); + let account = MailServices.accounts.createAccount(); + account.addIdentity(identity); + account.incomingServer = MailServices.accounts.createIncomingServer( + "test", + "localhost", + "pop3" + ); + + // Init nsIMsgSend and fields. + let msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + let compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + compFields.from = identity.email; + // Set the newsgroups filed so that the message will be passed to NntpService. + compFields.newsgroups = "foo.test"; + + let testFile = do_get_file("data/message1.eml"); + // Notice the second argument is accountKey. + await msgSend.sendMessageFile( + identity, + "", + compFields, + testFile, + false, + false, + Ci.nsIMsgSend.nsMsgDeliverNow, + null, + copyListener, + null, + null + ); + + // Make sure the messageFile passed to NntpService is the file we set above. + equal(MockNntpService.messageFile, testFile); + // Test accountKey passed to NntpService is correct. + equal(MockNntpService.accountKey, account.key); +}); diff --git a/comm/mailnews/compose/test/unit/test_attachment.js b/comm/mailnews/compose/test/unit/test_attachment.js new file mode 100644 index 0000000000..f0c5a4d91d --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_attachment.js @@ -0,0 +1,171 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for attachment file name. + */ + +var input0 = + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + + "`abcdefghijklmnopqrstuvwxyz{|}~" + + "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" + + "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf" + + "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf" + + "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf" + + "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef" + + "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff.txt"; + +// ascii only +var input1 = + "x!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + + "`abcdefghijklmnopqrstuvwxyz{|}~.txt"; + +var expectedCD0 = [ + "Content-Disposition: attachment;", + " filename*0*=UTF-8''%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31;", + " filename*1*=%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45;", + " filename*2*=%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59;", + " filename*3*=%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D;", + " filename*4*=%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%C2%A0;", + " filename*5*=%C2%A1%C2%A2%C2%A3%C2%A4%C2%A5%C2%A6%C2%A7%C2%A8%C2%A9%C2%AA;", + " filename*6*=%C2%AB%C2%AC%C2%AD%C2%AE%C2%AF%C2%B0%C2%B1%C2%B2%C2%B3%C2%B4;", + " filename*7*=%C2%B5%C2%B6%C2%B7%C2%B8%C2%B9%C2%BA%C2%BB%C2%BC%C2%BD%C2%BE;", + " filename*8*=%C2%BF%C3%80%C3%81%C3%82%C3%83%C3%84%C3%85%C3%86%C3%87%C3%88;", + " filename*9*=%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90%C3%91%C3%92;", + " filename*10*=%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%9B;", + " filename*11*=%C3%9C%C3%9D%C3%9E%C3%9F%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4;", + " filename*12*=%C3%A5%C3%A6%C3%A7%C3%A8%C3%A9%C3%AA%C3%AB%C3%AC%C3%AD;", + " filename*13*=%C3%AE%C3%AF%C3%B0%C3%B1%C3%B2%C3%B3%C3%B4%C3%B5%C3%B6;", + " filename*14*=%C3%B7%C3%B8%C3%B9%C3%BA%C3%BB%C3%BC%C3%BD%C3%BE%C3%BF%2E;", + " filename*15*=%74%78%74", + "", +].join("\r\n"); + +var expectedCD1 = + "Content-Disposition: attachment;\r\n" + + ' filename*0="x!\\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ";\r\n' + + ' filename*1="[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.txt"\r\n'; + +var ParamFoldingPref = { + // RFC2047: 0, + RFC2047WithCRLF: 1, + RFC2231: 2, +}; + +var expectedCTList0 = { + RFC2047: + "Content-Type: text/plain; charset=UTF-8;\r\n" + + ' name="=?UTF-8?B?ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJ?=' + + "=?UTF-8?Q?JKLMNOPQRSTUVWXYZ=5b=5c=5d=5e=5f=60abcdefghijklmnopqrstuvwx?=" + + "=?UTF-8?B?eXp7fH1+wqDCocKiwqPCpMKlwqbCp8KowqnCqsKrwqzCrcKuwq/CsMKx?=" + + "=?UTF-8?B?wrLCs8K0wrXCtsK3wrjCucK6wrvCvMK9wr7Cv8OAw4HDgsODw4TDhcOG?=" + + "=?UTF-8?B?w4fDiMOJw4rDi8OMw43DjsOPw5DDkcOSw5PDlMOVw5bDl8OYw5nDmsOb?=" + + "=?UTF-8?B?w5zDncOew5/DoMOhw6LDo8Okw6XDpsOnw6jDqcOqw6vDrMOtw67Dr8Ow?=" + + '=?UTF-8?B?w7HDssOzw7TDtcO2w7fDuMO5w7rDu8O8w73DvsO/LnR4dA==?="\r\n', + + RFC2047WithCRLF: + "Content-Type: text/plain; charset=UTF-8;\r\n" + + ' name="=?UTF-8?B?ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJ?=\r\n' + + " =?UTF-8?Q?JKLMNOPQRSTUVWXYZ=5b=5c=5d=5e=5f=60abcdefghijklmnopqrstuvwx?=\r\n" + + " =?UTF-8?B?eXp7fH1+wqDCocKiwqPCpMKlwqbCp8KowqnCqsKrwqzCrcKuwq/CsMKx?=\r\n" + + " =?UTF-8?B?wrLCs8K0wrXCtsK3wrjCucK6wrvCvMK9wr7Cv8OAw4HDgsODw4TDhcOG?=\r\n" + + " =?UTF-8?B?w4fDiMOJw4rDi8OMw43DjsOPw5DDkcOSw5PDlMOVw5bDl8OYw5nDmsOb?=\r\n" + + " =?UTF-8?B?w5zDncOew5/DoMOhw6LDo8Okw6XDpsOnw6jDqcOqw6vDrMOtw67Dr8Ow?=\r\n" + + ' =?UTF-8?B?w7HDssOzw7TDtcO2w7fDuMO5w7rDu8O8w73DvsO/LnR4dA==?="\r\n', + + RFC2231: "Content-Type: text/plain; charset=UTF-8\r\n", +}; + +var expectedCTList1 = { + RFC2047: + "Content-Type: text/plain; charset=UTF-8;\r\n" + + ' name="x!\\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.txt"\r\n', + + RFC2047WithCRLF: + "Content-Type: text/plain; charset=UTF-8;\r\n" + + ' name="x!\\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.txt"\r\n', + + RFC2231: "Content-Type: text/plain; charset=UTF-8\r\n", +}; + +function checkAttachment(expectedCD, expectedCT) { + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + let pos = msgData.indexOf("Content-Disposition:"); + Assert.notEqual(pos, -1); + let contentDisposition = msgData.substr(pos); + pos = 0; + do { + pos = contentDisposition.indexOf("\n", pos); + Assert.notEqual(pos, -1); + pos++; + } while (contentDisposition.startsWith(" ", pos)); + contentDisposition = contentDisposition.substr(0, pos); + Assert.equal(contentDisposition, expectedCD); + + pos = msgData.indexOf("Content-Type:"); // multipart + Assert.notEqual(pos, -1); + msgData = msgData.substr(pos + 13); + pos = msgData.indexOf("Content-Type:"); // body + Assert.notEqual(pos, -1); + msgData = msgData.substr(pos + 13); + pos = msgData.indexOf("Content-Type:"); // first attachment + Assert.notEqual(pos, -1); + var contentType = msgData.substr(pos); + pos = 0; + do { + pos = contentType.indexOf("\n", pos); + Assert.notEqual(pos, -1); + pos++; + } while (contentType.startsWith(" ", pos)); + contentType = contentType.substr(0, pos); + Assert.equal(contentType.toLowerCase(), expectedCT.toLowerCase()); +} + +async function testInput0() { + for (let folding in ParamFoldingPref) { + Services.prefs.setIntPref( + "mail.strictly_mime.parm_folding", + ParamFoldingPref[folding] + ); + await createMessage(input0); + checkAttachment(expectedCD0, expectedCTList0[folding]); + } +} + +async function testInput1() { + for (let folding in ParamFoldingPref) { + Services.prefs.setIntPref( + "mail.strictly_mime.parm_folding", + ParamFoldingPref[folding] + ); + await createMessage(input1); + checkAttachment(expectedCD1, expectedCTList1[folding]); + } +} + +var tests = [testInput0, testInput1]; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + tests.forEach(x => add_task(x)); + run_next_test(); +} + +/** + * Test that the full attachment content is used to pick the CTE. + */ +add_task(async function testBinaryAfterPlainTextAttachment() { + let testFile = do_get_file("data/binary-after-plain.txt"); + await createMessage(testFile); + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + // If only the first few chars are used, encoding will be incorrectly 7bit. + Assert.ok( + msgData.includes( + 'Content-Disposition: attachment; filename="binary-after-plain.txt"\r\nContent-Transfer-Encoding: base64\r\n' + ) + ); +}); diff --git a/comm/mailnews/compose/test/unit/test_attachment_intl.js b/comm/mailnews/compose/test/unit/test_attachment_intl.js new file mode 100644 index 0000000000..6ce352d4df --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_attachment_intl.js @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * attachment test using non-ascii character + */ + +let nonAsciiUrl = "http://\u65e5\u672c\u8a9e.jp"; +let prettyResult = "\u65e5\u672c\u8a9e.jp"; + +function doAttachmentUrlTest() { + // handles non-ascii url in nsIMsgAttachment + + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.url = nonAsciiUrl; + + Assert.equal(attachment.url, nonAsciiUrl); +} + +function doPrettyNameTest() { + // handles non-ascii url in nsIMsgCompose + + let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + msgCompose.initialize(params); + + Assert.equal( + msgCompose.AttachmentPrettyName(nonAsciiUrl, null), + prettyResult + ); +} + +function run_test() { + doAttachmentUrlTest(); + doPrettyNameTest(); + + do_test_finished(); +} diff --git a/comm/mailnews/compose/test/unit/test_autoReply.js b/comm/mailnews/compose/test/unit/test_autoReply.js new file mode 100644 index 0000000000..a81dc7bcef --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_autoReply.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests messages generated by ReplyWithTemplate. + */ + +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +load("../../../resources/logHelper.js"); // watch for errors in the error console + +const kSender = "from@foo.invalid"; + +var gIncomingMailFile = do_get_file("../../../data/bugmail10"); // mail to reply to +// reply-filter-testmail: mail to reply to (but not really) +var gIncomingMailFile2 = do_get_file("../../../data/reply-filter-testmail"); +// mail to reply to (but not really, no from) +var gIncomingMailFile3 = do_get_file("../../../data/mail-without-from"); +var gTemplateMailFile = do_get_file("../../../data/template-latin1"); // template +var gTemplateMailFile2 = do_get_file("../../../data/template-utf8"); // template2 +var gTemplateFolder; + +var gServer; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + gTemplateFolder = + localAccountUtils.rootFolder.createLocalSubfolder("Templates"); + + gServer = setupServerDaemon(); + gServer.start(); + + run_next_test(); +} + +add_task(async function copy_gIncomingMailFile() { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + // Copy gIncomingMailFile into the Inbox. + MailServices.copy.copyFileMessage( + gIncomingMailFile, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; +}); + +add_task(async function copy_gIncomingMailFile2() { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + // Copy gIncomingMailFile2 into the Inbox. + MailServices.copy.copyFileMessage( + gIncomingMailFile2, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; +}); + +add_task(async function copy_gIncomingMailFile3() { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + // Copy gIncomingMailFile3 into the Inbox. + MailServices.copy.copyFileMessage( + gIncomingMailFile3, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; +}); + +add_task(async function copy_gTemplateMailFile() { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + // Copy gTemplateMailFile into the Templates folder. + MailServices.copy.copyFileMessage( + gTemplateMailFile, + gTemplateFolder, + null, + true, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; +}); + +add_task(async function copy_gTemplateMailFile2() { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + // Copy gTemplateMailFile2 into the Templates folder. + MailServices.copy.copyFileMessage( + gTemplateMailFile2, + gTemplateFolder, + null, + true, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; +}); + +// Test that a reply is NOT sent when the message is not addressed to "me". +add_task(async function testReplyingToUnaddressedFails() { + try { + await testReply(0); // mail 0 is not to us! + do_throw("Replied to a message not addressed to us!"); + } catch (e) { + if (e.result != Cr.NS_ERROR_ABORT) { + throw e; + } + // Ok! We didn't reply to the message not specifically addressed to + // us (from@foo.invalid). + } +}); + +// Test that a reply is sent when the message is addressed to "me". +add_task(async function testReplyingToAdressedWorksLatin1() { + try { + await testReply(1); // mail 1 is addressed to us, using template-latin1 + } catch (e) { + do_throw("Didn't reply properly to a message addressed to us! " + e); + } +}); + +// Test that a reply is sent when the message is addressed to "me". +add_task(async function testReplyingToAdressedWorksUTF8() { + try { + await testReply(1, 1); // mail 1 is addressed to us, template-utf8 + } catch (e) { + do_throw("Didn't reply properly to a message addressed to us! " + e); + } +}); + +// Test that a reply is NOT even tried when the message has no From. +add_task(async function testReplyingToMailWithNoFrom() { + try { + await testReply(2); // mail 2 has no From + do_throw( + "Shouldn't even have tried to reply reply to the message " + + "with no From and no Reply-To" + ); + } catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } +}); + +// Test reply with template. +async function testReply(aHrdIdx, aTemplateHdrIdx = 0) { + let smtpServer = getBasicSmtpServer(); + smtpServer.port = gServer.port; + + let identity = getSmtpIdentity(kSender, smtpServer); + localAccountUtils.msgAccount.addIdentity(identity); + + let msgHdr = mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, aHrdIdx); + info( + "Msg#" + + aHrdIdx + + " author=" + + msgHdr.author + + ", recipients=" + + msgHdr.recipients + ); + let templateHdr = mailTestUtils.getMsgHdrN(gTemplateFolder, aTemplateHdrIdx); + + // See <method name="getTemplates"> in searchWidgets.xml + let msgTemplateUri = + gTemplateFolder.URI + + "?messageId=" + + templateHdr.messageId + + "&subject=" + + templateHdr.mime2DecodedSubject; + MailServices.compose.replyWithTemplate( + msgHdr, + msgTemplateUri, + null, + localAccountUtils.incomingServer + ); + + await TestUtils.waitForCondition(() => gServer._daemon.post); + let headers, body; + [headers, body] = MimeParser.extractHeadersAndBody(gServer._daemon.post); + Assert.ok(headers.get("Subject").startsWith("Auto: ")); + Assert.equal(headers.get("Auto-submitted"), "auto-replied"); + Assert.equal(headers.get("In-Reply-To"), "<" + msgHdr.messageId + ">"); + Assert.equal(headers.get("References"), "<" + msgHdr.messageId + ">"); + // XXX: something's wrong with how the fake server gets the data. + // The text gets converted to UTF-8 (regardless of what it is) at some point. + // Suspect a bug with how BinaryInputStream handles the strings. + if (templateHdr.charset == "windows-1252") { + // XXX: should really check for "åäö xlatin1" + if (!body.includes("åäö xlatin1")) { + // template-latin1 contains this + do_throw( + "latin1 body didn't go through! hdr msgid=" + + templateHdr.messageId + + ", msgbody=" + + body + ); + } + } else if (templateHdr.charset == "utf-8") { + // XXX: should really check for "åäö xutf8" + if (!body.includes("åäö xutf8")) { + // template-utf8 contains this + do_throw( + "utf8 body didn't go through! hdr msgid=" + + templateHdr.messageId + + ", msgbody=" + + body + ); + } + } else if (templateHdr.charset) { + do_throw( + "unexpected msg charset: " + + templateHdr.charset + + ", hdr msgid=" + + templateHdr.messageId + ); + } else { + do_throw("didn't find a msg charset! hdr msgid=" + templateHdr.messageId); + } + gServer.resetTest(); +} + +add_task(function teardown() { + // fake server cleanup + gServer.stop(); +}); diff --git a/comm/mailnews/compose/test/unit/test_bcc.js b/comm/mailnews/compose/test/unit/test_bcc.js new file mode 100644 index 0000000000..3689b920e7 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_bcc.js @@ -0,0 +1,330 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/** + * Test that when bcc field is set, bcc header should not exist in the sent + * mail, but should exist in the mail copy (e.g. Sent folder). + */ + +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gServer; +var gSentFolder; + +function cleanUpSent() { + let messages = [...gSentFolder.msgDatabase.enumerateMessages()]; + if (messages.length) { + gSentFolder.deleteMessages(messages, null, true, false, null, false); + } +} + +/** + * Load local mail account and start fake SMTP server. + */ +add_setup(async function setup() { + localAccountUtils.loadLocalMailAccount(); + gServer = setupServerDaemon(); + gServer.start(); + registerCleanupFunction(() => { + gServer.stop(); + }); + gSentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent"); +}); + +/** + * Send a msg with bcc field set, then check the sent mail doesn't contain bcc + * header, but the mail saved to the Sent folder contains bcc header. + */ +add_task(async function testBcc() { + gServer.resetTest(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer(gServer.port) + ); + + // Prepare the comp fields, including the bcc field. + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.to = "Nobody <to@tinderbox.invalid>"; + fields.subject = "Test bcc"; + fields.bcc = "bcc@tinderbox.invalid"; + fields.body = "A\r\nBcc: \r\n mail body\r\n."; + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + + // Send the mail. + let msgCompose = MailServices.compose.initCompose(params); + msgCompose.type = Ci.nsIMsgCompType.New; + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgDeliverNow, + identity, + "", + null, + progress + ); + await promise; + + let expectedBody = `\r\n\r\n${fields.body}`; + // Should not contain extra \r\n between head and body. + let notExpectedBody = `\r\n\r\n\r\n${fields.body}`; + + Assert.ok(gServer._daemon.post.includes("Subject: Test bcc")); + // Check that bcc header doesn't exist in the sent mail. + Assert.ok(!gServer._daemon.post.includes("Bcc: bcc@tinderbox.invalid")); + Assert.ok(gServer._daemon.post.includes(expectedBody)); + Assert.ok(!gServer._daemon.post.includes(notExpectedBody)); + + let msgData = mailTestUtils.loadMessageToString( + gSentFolder, + mailTestUtils.getMsgHdrN(gSentFolder, 0) + ); + Assert.ok(msgData.includes("Subject: Test bcc")); + // Check that bcc header exists in the mail copy. + Assert.ok(msgData.includes("Bcc: bcc@tinderbox.invalid")); + Assert.ok(msgData.includes(fields.body)); + Assert.ok(msgData.includes(expectedBody)); + Assert.ok(!msgData.includes(notExpectedBody)); +}); + +/** + * Test that non-utf8 eml attachment is intact after sent to a bcc recipient. + */ +add_task(async function testBccWithNonUtf8EmlAttachment() { + gServer.resetTest(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer(gServer.port) + ); + + // Prepare the comp fields, including the bcc field. + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.to = "Nobody <to@tinderbox.invalid>"; + fields.subject = "Test bcc with non-utf8 eml attachment"; + fields.bcc = "bcc@tinderbox.invalid"; + + let testFile = do_get_file("data/shift-jis.eml"); + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.url = "file://" + testFile.path; + attachment.contentType = "message/rfc822"; + attachment.name = testFile.leafName; + fields.addAttachment(attachment); + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + + // Send the mail. + let msgCompose = MailServices.compose.initCompose(params); + msgCompose.type = Ci.nsIMsgCompType.New; + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgDeliverNow, + identity, + "", + null, + progress + ); + await promise; + + Assert.ok( + gServer._daemon.post.includes( + "Subject: Test bcc with non-utf8 eml attachment" + ) + ); + // \x8C\xBB\x8B\xB5 is 現況 in SHIFT-JIS. + Assert.ok(gServer._daemon.post.includes("\r\n\r\n\x8C\xBB\x8B\xB5\r\n")); +}); + +add_task(async function testBccWithSendLater() { + gServer.resetTest(); + cleanUpSent(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer(gServer.port) + ); + let account = MailServices.accounts.createAccount(); + account.addIdentity(identity); + + // Prepare the comp fields, including the bcc field. + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.to = "Nobody <to@tinderbox.invalid>"; + fields.subject = "Test bcc with send later"; + fields.bcc = "bcc@tinderbox.invalid"; + fields.body = "A\r\nBcc: \r\n mail body\r\n."; + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + + // Queue the mail to send later. + let msgCompose = MailServices.compose.initCompose(params); + msgCompose.type = Ci.nsIMsgCompType.New; + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgQueueForLater, + identity, + "", + null, + progress + ); + await promise; + + let onStopSendingPromise = PromiseUtils.defer(); + let msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater + ); + let sendLaterListener = { + onStartSending() {}, + onMessageStartSending() {}, + onMessageSendProgress() {}, + onMessageSendError() {}, + onStopSending() { + let expectedBody = `\r\n\r\n${fields.body}`; + // Should not contain extra \r\n between head and body. + let notExpectedBody = `\r\n\r\n\r\n${fields.body}`; + + Assert.ok(gServer._daemon.post.includes(`Subject: ${fields.subject}`)); + // Check that bcc header doesn't exist in the sent mail. + Assert.ok(!gServer._daemon.post.includes("Bcc: bcc@tinderbox.invalid")); + Assert.ok(gServer._daemon.post.includes(expectedBody)); + Assert.ok(!gServer._daemon.post.includes(notExpectedBody)); + + let msgData = mailTestUtils.loadMessageToString( + gSentFolder, + mailTestUtils.getMsgHdrN(gSentFolder, 0) + ); + Assert.ok(msgData.includes(`Subject: ${fields.subject}`)); + // Check that bcc header exists in the mail copy. + Assert.ok(msgData.includes("Bcc: bcc@tinderbox.invalid")); + Assert.ok(msgData.includes(fields.body)); + Assert.ok(msgData.includes(expectedBody)); + Assert.ok(!msgData.includes(notExpectedBody)); + + msgSendLater.removeListener(sendLaterListener); + onStopSendingPromise.resolve(); + }, + }; + + msgSendLater.addListener(sendLaterListener); + + // Actually send the message. + msgSendLater.sendUnsentMessages(identity); + await onStopSendingPromise.promise; +}); + +/** + * Test that sending bcc only message from Outbox works. With a bcc only + * message, nsMsgSendLater passes `To: undisclosed-recipients: ;` to + * SmtpService, but it should not be sent to the SMTP server. + */ +add_task(async function testBccOnlyWithSendLater() { + gServer.resetTest(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer(gServer.port) + ); + let account = MailServices.accounts.createAccount(); + account.addIdentity(identity); + + // Prepare the comp fields, including the bcc field. + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.subject = "Test bcc only with send later"; + fields.bcc = "bcc@tinderbox.invalid"; + fields.body = "A\r\nBcc: \r\n mail body\r\n."; + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + + // Queue the mail to send later. + let msgCompose = MailServices.compose.initCompose(params); + msgCompose.type = Ci.nsIMsgCompType.New; + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgQueueForLater, + identity, + "", + null, + progress + ); + await promise; + + let onStopSendingPromise = PromiseUtils.defer(); + let msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater + ); + let sendLaterListener = { + onStartSending() {}, + onMessageStartSending() {}, + onMessageSendProgress() {}, + onMessageSendError() {}, + onStopSending() { + // Should not include RCPT TO:<undisclosed-recipients: ;> + do_check_transaction(gServer.playTransaction(), [ + "EHLO test", + `MAIL FROM:<from@tinderbox.invalid> BODY=8BITMIME SIZE=${gServer._daemon.post.length}`, + "RCPT TO:<bcc@tinderbox.invalid>", + "DATA", + ]); + + msgSendLater.removeListener(sendLaterListener); + onStopSendingPromise.resolve(); + }, + }; + + msgSendLater.addListener(sendLaterListener); + + // Actually send the message. + msgSendLater.sendUnsentMessages(identity); + await onStopSendingPromise.promise; +}); diff --git a/comm/mailnews/compose/test/unit/test_bug155172.js b/comm/mailnews/compose/test/unit/test_bug155172.js new file mode 100644 index 0000000000..06c14416a7 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_bug155172.js @@ -0,0 +1,140 @@ +/** + * Authentication tests for SMTP. + */ + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/alertTestUtils.js"); +load("../../../resources/passwordStorage.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gNewPassword = null; + +// for alertTestUtils.js +function confirmExPS( + parent, + aDialogTitle, + aText, + aButtonFlags, + aButton0Title, + aButton1Title, + aButton2Title, + aCheckMsg, + aCheckState +) { + // Just return 2 which will is pressing button 2 - enter a new password. + return 2; +} + +function promptPasswordPS( + aParent, + aDialogTitle, + aText, + aPassword, + aCheckMsg, + aCheckState +) { + aPassword.value = gNewPassword; + return true; +} + +var server; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var kUsername = "test.smtp@fakeserver"; +// kPasswordSaved is the one defined in signons-smtp.json, the other one +// is intentionally wrong. +var kPasswordWrong = "wrong"; +var kPasswordSaved = "smtptest"; + +add_task(async function () { + registerAlertTestUtils(); + + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + // Username needs to match the login information stored in the signons json + // file. + handler.kUsername = kUsername; + handler.kPassword = kPasswordWrong; + handler.kAuthRequired = true; + handler.kAuthSchemes = ["PLAIN", "LOGIN"]; // make match expected transaction below + return handler; + } + + server = setupServerDaemon(createHandler); + server.setDebugLevel(fsDebugAll); + + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-smtp.json"); + + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // This time with auth + test = "Auth sendMailMessage"; + + smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext; + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + // Set the new password for when we get a prompt + gNewPassword = kPasswordWrong; + + await urlListener.promise; + + var transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPasswordSaved), + "AUTH LOGIN", + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPasswordWrong), + "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_bug474774.js b/comm/mailnews/compose/test/unit/test_bug474774.js new file mode 100644 index 0000000000..ba0c20667c --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_bug474774.js @@ -0,0 +1,253 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Tests bug 474774 - assertions when saving send later and when sending with + * FCC switched off. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var server; +var smtpServer; +var originalData; +var finished = false; +var identity = null; + +var testFile = do_get_file("data/429891_testcase.eml"); + +var kTestFileSender = "from_A@foo.invalid"; +var kTestFileRecipient = "to_A@foo.invalid"; + +var kIdentityMail = "identity@foo.invalid"; + +var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater +); + +// This listener handles the post-sending of the actual message and checks the +// sequence and ensures the data is correct. +function msll() {} + +msll.prototype = { + _initialTotal: 0, + + // nsIMsgSendLaterListener + onStartSending(aTotalMessageCount) { + this._initialTotal = 1; + Assert.equal(msgSendLater.sendingMessages, true); + }, + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) {}, + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) { + // XXX Enable this function + }, + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + do_throw( + "onMessageSendError should not have been called, status: " + aStatus + ); + }, + onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) { + print("msll onStopSending\n"); + try { + Assert.equal(aSuccessful, 1); + Assert.equal(aStatus, 0); + Assert.equal(aTotalTried, 1); + Assert.equal(this._initialTotal, 1); + Assert.equal(msgSendLater.sendingMessages, false); + + do_check_transaction(server.playTransaction(), [ + "EHLO test", + "MAIL FROM:<" + + kTestFileSender + + "> BODY=8BITMIME SIZE=" + + originalData.length, + "RCPT TO:<" + kTestFileRecipient + ">", + "DATA", + ]); + + // Compare data file to what the server received + Assert.equal(originalData, server._daemon.post); + + // Now wait till the copy is finished for the sent message + do_test_pending(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } + do_test_finished(); + }, +}; + +/* exported OnStopCopy */ +// for head_compose.js +function OnStopCopy(aStatus) { + do_test_finished(); + + try { + Assert.equal(aStatus, 0); + + // Check this is false before we start sending + Assert.equal(msgSendLater.sendingMessages, false); + + let folder = msgSendLater.getUnsentMessagesFolder(identity); + + // Check we have a message in the unsent message folder + Assert.equal(folder.getTotalMessages(false), 1); + + // Now do a comparison of what is in the sent mail folder + let msgData = mailTestUtils.loadMessageToString( + folder, + mailTestUtils.firstMsgHdr(folder) + ); + + // Skip the headers etc that mailnews adds + var pos = msgData.indexOf("From:"); + Assert.notEqual(pos, -1); + + msgData = msgData.substr(pos); + + // Check the data is matching. + Assert.equal(originalData, msgData); + + do_test_pending(); + sendMessageLater(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + finished = true; + } +} + +// This function does the actual send later +function sendMessageLater() { + do_test_finished(); + + // Set up the SMTP server. + server = setupServerDaemon(); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server + server.start(); + smtpServer.port = server.port; + + // A test to check that we are sending files correctly, including checking + // what the server receives and what we output. + test = "sendMessageLater"; + + var messageListener = new msll(); + + msgSendLater.addListener(messageListener); + + // Send the unsent message + msgSendLater.sendUnsentMessages(identity); + + server.performTest(); + + do_timeout(10000, function () { + if (!finished) { + do_throw("Notifications of message send/copy not received"); + } + }); + + do_test_pending(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +} + +add_task(async function run_the_test() { + // Test file - for bug 429891 + originalData = await IOUtils.readUTF8(testFile.path); + + // Ensure we have a local mail account, an normal account and appropriate + // servers and identities. + localAccountUtils.loadLocalMailAccount(); + + MailServices.accounts.setSpecialFolders(); + + let account = MailServices.accounts.createAccount(); + let incomingServer = MailServices.accounts.createIncomingServer( + "test", + "localhost", + "pop3" + ); + + smtpServer = getBasicSmtpServer(0); + identity = getSmtpIdentity(kIdentityMail, smtpServer); + + account.addIdentity(identity); + account.defaultIdentity = identity; + account.incomingServer = incomingServer; + MailServices.accounts.defaultAccount = account; + + localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + identity.doFcc = false; + + // Now prepare to actually "send" the message later, i.e. dump it in the + // unsent messages folder. + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // Setting the compFields sender and recipient to any value is required to + // survive mime_sanity_check_fields in nsMsgCompUtils.cpp. + // Sender and recipient are required for sendMessageFile but SMTP + // transaction values will be used directly from mail body. + compFields.from = "irrelevant@foo.invalid"; + compFields.to = "irrelevant@foo.invalid"; + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + msgSend.sendMessageFile( + identity, + "", + compFields, + testFile, + false, + false, + Ci.nsIMsgSend.nsMsgQueueForLater, + null, + copyListener, + null, + null + ); + + // Now we wait till we get copy notification of completion. + do_test_pending(); +}); diff --git a/comm/mailnews/compose/test/unit/test_createAndSendMessage.js b/comm/mailnews/compose/test/unit/test_createAndSendMessage.js new file mode 100644 index 0000000000..41daecf2cc --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_createAndSendMessage.js @@ -0,0 +1,170 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/** + * Test createAndSendMessage creates a mail file when not using the editor. + */ + +var server; +var sentFolder; +const originalData = "createAndSendMessage utf-8 test åäöÅÄÖ"; +// This is the originalData converted to a byte string. +const expectedData = "createAndSendMessage utf-8 test åäöÃ\x85Ã\x84Ã\x96"; +const expectedContentTypeHeaders = + "Content-Type: text/plain; charset=UTF-8; format=flowed\r\nContent-Transfer-Encoding: 8bit\r\n\r\n"; +var finished = false; + +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; + +function checkData(msgData) { + // Skip the headers etc that mailnews adds + var pos = msgData.indexOf("Content-Type:"); + Assert.notEqual(pos, -1); + + msgData = msgData.substr(pos); + + Assert.equal(msgData, expectedContentTypeHeaders + expectedData + "\r\n"); +} + +function MessageListener() {} + +MessageListener.prototype = { + // nsIMsgSendListener + onStartSending(aMsgID, aMsgSize) {}, + onProgress(aMsgID, aProgress, aProgressMax) {}, + onStatus(aMsgID, aMsg) {}, + onStopSending(aMsgID, aStatus, aMsg, aReturnFile) { + try { + Assert.equal(aStatus, 0); + + // Compare data file to what the server received + checkData(server._daemon.post); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(false); + } + } + }, + onGetDraftFolderURI(aMsgID, aFolderURI) {}, + onSendNotPerformed(aMsgID, aStatus) {}, + onTransportSecurityError(msgID, status, secInfo, location) {}, + + // nsIMsgCopyServiceListener + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + GetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + Assert.equal(aStatus, 0); + try { + // Now do a comparison of what is in the sent mail folder + let msgData = mailTestUtils.loadMessageToString( + sentFolder, + mailTestUtils.firstMsgHdr(sentFolder) + ); + + checkData(msgData); + } catch (e) { + do_throw(e); + } finally { + finished = true; + do_test_finished(); + } + }, + + // QueryInterface + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgSendListener", + "nsIMsgCopyServiceListener", + ]), +}; + +/** + * Call createAndSendMessage, expect onStopSending to be called. + */ +add_task(async function testCreateAndSendMessage() { + server = setupServerDaemon(); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + MailServices.accounts.setSpecialFolders(); + + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kSender, smtpServer); + + sentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + Assert.equal(identity.doFcc, true); + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // A test to check that we are sending files correctly, including checking + // what the server receives and what we output. + test = "sendMessageFile"; + + // Msg Comp Fields + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + compFields.from = identity.email; + compFields.to = kTo; + + var messageListener = new MessageListener(); + + msgSend.createAndSendMessage( + null, + identity, + "", + compFields, + false, + false, + Ci.nsIMsgSend.nsMsgDeliverNow, + null, + "text/plain", + // The following parameter is the message body, test that utf-8 is handled + // correctly. + originalData, + null, + null, + messageListener, + null, + null, + Ci.nsIMsgCompType.New + ); + + server.performTest(); + + do_timeout(10000, function () { + if (!finished) { + do_throw("Notifications of message send/copy not received"); + } + }); + + do_test_pending(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_createRFC822Message.js b/comm/mailnews/compose/test/unit/test_createRFC822Message.js new file mode 100644 index 0000000000..9502031484 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_createRFC822Message.js @@ -0,0 +1,68 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/** + * Test createRFC822Message creates a mail file. + */ + +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +let customSendListener = { + ...copyListener, + OnStopCopy() {}, + + /** + * Test a mail file is created and has correct content. + */ + async onStopSending(msgId, status, msg, returnFile) { + ok(returnFile.exists(), "createRFC822Message should create a mail file"); + let content = await IOUtils.read(returnFile.path); + content = String.fromCharCode(...content); + ok( + content.includes("Subject: Test createRFC822Message\r\n"), + "Mail file should contain correct subject line" + ); + ok( + content.includes( + "createRFC822Message is used by nsImportService \xe4\xe9" + ), + "Mail file should contain correct body" + ); + do_test_finished(); + }, +}; + +/** + * Call createRFC822Message, expect onStopSending to be called. + */ +add_task(async function testCreateRFC822Message() { + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.from = "Somebody <somebody@tinderbox.invalid>"; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test createRFC822Message"; + + let msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + msgSend.createRFC822Message( + identity, + fields, + "text/plain", + // The following parameter is the message body that can contain arbitrary + // binary data, let's try some windows-1252 data (äé). + "createRFC822Message is used by nsImportService \xe4\xe9", + true, // isDraft + [], + [], + customSendListener + ); + do_test_pending(); +}); diff --git a/comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js b/comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js new file mode 100644 index 0000000000..27e879d018 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_detectAttachmentCharset.js @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for auto-detecting attachment file charset. + */ + +function checkAttachmentCharset(expectedCharset) { + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + let attachmentData = getAttachmentFromContent(msgData); + + Assert.equal(expectedCharset, getContentCharset(attachmentData)); +} + +function getContentCharset(aContent) { + let found = aContent.match(/^Content-Type: text\/plain; charset=(.*?);/); + if (found) { + Assert.equal(found.length, 2); + return found[1]; + } + return null; +} + +async function testUTF8() { + await createMessage(do_get_file("data/test-UTF-8.txt")); + checkAttachmentCharset("UTF-8"); +} + +async function testUTF16BE() { + await createMessage(do_get_file("data/test-UTF-16BE.txt")); + checkAttachmentCharset("UTF-16BE"); +} + +async function testUTF16LE() { + await createMessage(do_get_file("data/test-UTF-16LE.txt")); + checkAttachmentCharset("UTF-16LE"); +} + +async function testShiftJIS() { + await createMessage(do_get_file("data/test-SHIFT_JIS.txt")); + checkAttachmentCharset("Shift_JIS"); +} + +async function testISO2022JP() { + await createMessage(do_get_file("data/test-ISO-2022-JP.txt")); + checkAttachmentCharset("ISO-2022-JP"); +} + +async function testKOI8R() { + // NOTE: KOI8-R is detected as KOI8-U which is a superset covering both + // Russian and Ukrainian (a few box-drawing characters are repurposed). + await createMessage(do_get_file("data/test-KOI8-R.txt")); + checkAttachmentCharset("KOI8-U"); +} + +async function testWindows1252() { + await createMessage(do_get_file("data/test-windows-1252.txt")); + checkAttachmentCharset("windows-1252"); +} + +var tests = [ + testUTF8, + testUTF16BE, + testUTF16LE, + testShiftJIS, + testISO2022JP, + testKOI8R, + testWindows1252, +]; + +function run_test() { + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + Services.prefs.setIntPref("mail.strictly_mime.parm_folding", 0); + + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/compose/test/unit/test_expandMailingLists.js b/comm/mailnews/compose/test/unit/test_expandMailingLists.js new file mode 100644 index 0000000000..aa5998196f --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_expandMailingLists.js @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/** + * Tests nsMsgCompose expandMailingLists. + */ + +var MsgComposeContractID = "@mozilla.org/messengercompose/compose;1"; +var MsgComposeParamsContractID = + "@mozilla.org/messengercompose/composeparams;1"; +var MsgComposeFieldsContractID = + "@mozilla.org/messengercompose/composefields;1"; +var nsIMsgCompose = Ci.nsIMsgCompose; +var nsIMsgComposeParams = Ci.nsIMsgComposeParams; +var nsIMsgCompFields = Ci.nsIMsgCompFields; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * Helper to check population worked as expected. + * + * @param {string} aTo - Text in the To field. + * @param {string} aCheckTo - The expected To addresses (after possible list population). + */ +function checkPopulate(aTo, aCheckTo) { + let msgCompose = Cc[MsgComposeContractID].createInstance(nsIMsgCompose); + + // Set up some basic fields for compose. + let fields = Cc[MsgComposeFieldsContractID].createInstance(nsIMsgCompFields); + + fields.to = aTo; + + // Set up some params + let params = + Cc[MsgComposeParamsContractID].createInstance(nsIMsgComposeParams); + + params.composeFields = fields; + + msgCompose.initialize(params); + + msgCompose.expandMailingLists(); + equal(fields.to, aCheckTo); +} + +function run_test() { + loadABFile("data/listexpansion", kPABData.fileName); + + // XXX Getting all directories ensures we create all ABs because mailing + // lists need help initialising themselves + MailServices.ab.directories; + + // Test expansion of list with no description. + checkPopulate( + "simpson <simpson>", + 'Simpson <homer@example.com>, Marge <marge@example.com>, Bart <bart@foobar.invalid>, "lisa@example.com" <lisa@example.com>' + ); + + // Test expansion fo list with description. + checkPopulate( + "marge <marges own list>", + "Simpson <homer@example.com>, Marge <marge@example.com>" + ); + + // Special tests for bug 1287726: Lists in list. This is what the data looks like: + // 1) family (list) = parents (list) + kids (list). + // 2) parents (list) = homer + marge + parents (list recursion). + // 3) kids (list) = older-kids (list) + maggie. + // 4) older-kids (list) = bart + lisa. + // 5) bad-kids (list) = older-kids + bad-younger-kids (list). + // 6) bad-younger-kids (list) = maggie + bad-kids (list recursion). + checkPopulate( + "family <family>", + "Simpson <homer@example.com>, Marge <marge@example.com>, " + + '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>, Maggie <maggie@example.com>' + ); + checkPopulate( + "parents <parents>", + "Simpson <homer@example.com>, Marge <marge@example.com>" + ); + checkPopulate( + "kids <kids>", + '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>, ' + + "Maggie <maggie@example.com>" + ); + checkPopulate( + "older-kids <older-kids>", + '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>' + ); + checkPopulate( + "bad-kids <bad-kids>", + '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>, ' + + "Maggie <maggie@example.com>" + ); + checkPopulate( + "bad-younger-kids <bad-younger-kids>", + "Maggie <maggie@example.com>, " + + '"lisa@example.com" <lisa@example.com>, Bart <bart@foobar.invalid>' + ); + + // Test we don't mistake an email address for a list, with a few variations. + checkPopulate("Simpson <homer@example.com>", "Simpson <homer@example.com>"); + checkPopulate("simpson <homer@example.com>", "simpson <homer@example.com>"); + checkPopulate( + "simpson <homer@not-in-ab.invalid>", + "simpson <homer@not-in-ab.invalid>" + ); + + checkPopulate("Marge <marge@example.com>", "Marge <marge@example.com>"); + checkPopulate("marge <marge@example.com>", "marge <marge@example.com>"); + checkPopulate( + "marge <marge@not-in-ab.invalid>", + "marge <marge@not-in-ab.invalid>" + ); +} diff --git a/comm/mailnews/compose/test/unit/test_fcc2.js b/comm/mailnews/compose/test/unit/test_fcc2.js new file mode 100644 index 0000000000..e7f8d7aadf --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_fcc2.js @@ -0,0 +1,47 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/* + * Test that when fcc2 field is set, the mail is copied to the fcc2 folder. + */ + +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +let fcc2Folder; + +add_setup(async function () { + localAccountUtils.loadLocalMailAccount(); + fcc2Folder = localAccountUtils.rootFolder.createLocalSubfolder("fcc2"); +}); + +/** + * Send a message with the fcc2 field set, then check the message in the fcc2 + * folder. + */ +add_task(async function testFcc2() { + let CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields + ); + let fields = new CompFields(); + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test fcc2"; + fields.fcc2 = fcc2Folder.URI; + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + await richCreateMessage(fields, [], identity); + + // Check the message shows up correctly in the fcc2 folder. + let msgData = mailTestUtils.loadMessageToString( + fcc2Folder, + mailTestUtils.firstMsgHdr(fcc2Folder) + ); + Assert.ok(msgData.includes("Subject: Test fcc2")); +}); + +add_task(async function cleanup() { + fcc2Folder.deleteSelf(null); +}); diff --git a/comm/mailnews/compose/test/unit/test_fccReply.js b/comm/mailnews/compose/test/unit/test_fccReply.js new file mode 100644 index 0000000000..b7e44bce17 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_fccReply.js @@ -0,0 +1,140 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/* + * Test that when nsIMsgIdentity.fccReplyFollowsParent is true, the reply mail + * is copied to the same folder as the original mail. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +var gServer; + +/** + * Send a reply to originalMsgURI. + */ +async function sendReply(identity, fields, originalMsgURI, compType) { + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + params.originalMsgURI = originalMsgURI; + let msgCompose = MailServices.compose.initCompose(params); + msgCompose.type = compType; + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgDeliverNow, + identity, + "", + null, + progress + ); + return promise; +} + +/** + * Load local mail account and start fake SMTP server. + */ +add_setup(function () { + localAccountUtils.loadLocalMailAccount(); + gServer = setupServerDaemon(); + gServer.start(); + registerCleanupFunction(() => { + gServer.stop(); + }); +}); + +/** + * With fccReplyFollowsParent enabled, send a few replies then check the replies + * exists in the Inbox folder. + */ +add_task(async function testFccReply() { + // Turn on fccReplyFollowsParent. + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer(gServer.port) + ); + identity.fccReplyFollowsParent = true; + + // Copy a test mail into the Inbox. + let file = do_get_file("data/message1.eml"); // mail to reply to + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; + + let CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields + ); + let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder); + let originalMsgURI = msgHdr.folder.getUriForMsg(msgHdr); + + // Test nsIMsgCompFields.Reply. + let fields = new CompFields(); + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test fcc reply"; + await sendReply(identity, fields, originalMsgURI, Ci.nsIMsgCompType.Reply); + await TestUtils.waitForCondition(() => gServer._daemon.post); + let msgData = mailTestUtils.loadMessageToString( + localAccountUtils.inboxFolder, + mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, 1) + ); + Assert.ok(msgData.includes("Subject: Test fcc reply")); + + // Test nsIMsgCompFields.ReplyToGroup. + gServer.resetTest(); + fields.subject = "Test fccReplyToGroup"; + await sendReply( + identity, + fields, + originalMsgURI, + Ci.nsIMsgCompType.ReplyToGroup + ); + await TestUtils.waitForCondition(() => gServer._daemon.post); + msgData = mailTestUtils.loadMessageToString( + localAccountUtils.inboxFolder, + mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, 2) + ); + Assert.ok(msgData.includes("Subject: Test fccReplyToGroup")); + + // Test nsIMsgCompFields.ReplyToList. + gServer.resetTest(); + fields.subject = "Test fccReplyToList"; + await sendReply( + identity, + fields, + originalMsgURI, + Ci.nsIMsgCompType.ReplyToList + ); + await TestUtils.waitForCondition(() => gServer._daemon.post); + msgData = mailTestUtils.loadMessageToString( + localAccountUtils.inboxFolder, + mailTestUtils.getMsgHdrN(localAccountUtils.inboxFolder, 3) + ); + Assert.ok(msgData.includes("Subject: Test fccReplyToList")); +}); diff --git a/comm/mailnews/compose/test/unit/test_longLines.js b/comm/mailnews/compose/test/unit/test_longLines.js new file mode 100644 index 0000000000..cd75e75d38 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_longLines.js @@ -0,0 +1,232 @@ +/* + * Test ensuring that messages with "long lines" are transmitted correctly. + * Most of this test was copied from test_messageHeaders.js. + */ + +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +var CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields +); + +// Copied from jsmime.js. +function stringToTypedArray(buffer) { + var typedarray = new Uint8Array(buffer.length); + for (var i = 0; i < buffer.length; i++) { + typedarray[i] = buffer.charCodeAt(i); + } + return typedarray; +} + +function checkDraftHeadersAndBody( + expectedHeaders, + expectedBody, + charset = "UTF-8" +) { + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + checkMessageHeaders(msgData, expectedHeaders); + + // Get the message body, decode from base64 and check. + let endOfHeaders = msgData.indexOf("\r\n\r\n"); + let body = msgData.slice(endOfHeaders + 4); + let endOfBody = body.indexOf("\r\n\r\n"); + + if (endOfBody > 0) { + body = body.slice(0, endOfBody); + } else { + body = body.slice(0, body.length); + } + + // Remove line breaks and decode from base64 if required. + if (expectedHeaders["Content-Transfer-Encoding"] == "base64") { + body = atob(body.replace(/\r\n/g, "")); + } + + if (charset == "UTF-8") { + let expectedBinary = String.fromCharCode.apply( + undefined, + new TextEncoder("UTF-8").encode(expectedBody) + ); + Assert.equal(body, expectedBinary); + } else { + let strView = stringToTypedArray(body); + let decodedBody = new TextDecoder(charset).decode(strView); + Assert.equal(decodedBody, expectedBody); + } +} + +function checkMessageHeaders(msgData, expectedHeaders, partNum = "") { + let seen = false; + let handler = { + startPart(part, headers) { + if (part != partNum) { + return; + } + seen = true; + for (let header in expectedHeaders) { + let expected = expectedHeaders[header]; + if (expected === undefined) { + Assert.ok(!headers.has(header)); + } else { + let value = headers.getRawHeader(header); + Assert.equal(value.length, 1); + value[0] = value[0].replace(/boundary=[^;]*(;|$)/, "boundary=."); + Assert.equal(value[0], expected); + } + } + }, + }; + MimeParser.parseSync(msgData, handler, { + onerror(e) { + throw e; + }, + }); + Assert.ok(seen); +} + +// Create a line with 600 letters 'a' with acute accent, encoded as +// two bytes c3a1 in UTF-8. +let longMultibyteLine = "\u00E1".repeat(600); + +// And here a line with a Korean character, encoded as three bytes +// ec9588 in UTF-8. +let longMultibyteLineCJK = "안".repeat(400); + +// And some Japanese. +let longMultibyteLineJapanese = "語".repeat(450); + +async function testBodyWithLongLine() { + // Lines in the message body are split by CRLF according to RFC 5322, should + // be independent of the system. + let newline = "\r\n"; + + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + identity.fullName = "Me"; + identity.organization = "World Destruction Committee"; + fields.from = "Nobody <nobody@tinderbox.invalid>"; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Message with 1200 byte line in body"; + let htmlMessage = + "<html><head>" + + '<meta http-equiv="content-type" content="text/html; charset=utf-8">' + + "</head><body>" + + longMultibyteLine + + "</body></html>\r\n\r\n"; + fields.body = htmlMessage; + await richCreateMessage(fields, [], identity); + checkDraftHeadersAndBody( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "base64", + }, + htmlMessage + ); + + // Again, but this time as plain text. + fields.body = htmlMessage; + fields.forcePlainText = true; + fields.useMultipartAlternative = false; + await richCreateMessage(fields, [], identity); + checkDraftHeadersAndBody( + { + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "base64", + }, + longMultibyteLine + " " + newline + newline // Expected body: The message without the tags. + ); + + // Now CJK. + fields.forcePlainText = false; + htmlMessage = + "<html><head>" + + '<meta http-equiv="content-type" content="text/html; charset=utf-8">' + + "</head><body>" + + longMultibyteLineCJK + + "</body></html>\r\n\r\n"; + fields.body = htmlMessage; + await richCreateMessage(fields, [], identity); + checkDraftHeadersAndBody( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "base64", + }, + htmlMessage + ); + + // Again, but this time as plain text. + fields.body = htmlMessage; + fields.forcePlainText = true; + fields.useMultipartAlternative = false; + await richCreateMessage(fields, [], identity); + checkDraftHeadersAndBody( + { + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "base64", + }, + longMultibyteLineCJK + " " + newline + newline // Expected body: The message without the tags. + ); + + // Now a test for ISO-2022-JP. + fields.forcePlainText = false; + htmlMessage = + "<html><head>" + + '<meta http-equiv="content-type" content="text/html; charset=ISO-2022-JP">' + + "</head><body>" + + longMultibyteLineJapanese + + "</body></html>\r\n\r\n"; + fields.body = htmlMessage; + await richCreateMessage(fields, [], identity); + checkDraftHeadersAndBody( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "base64", + }, + htmlMessage + ); + + // Again, but this time as plain text. + fields.body = htmlMessage; + fields.forcePlainText = true; + fields.useMultipartAlternative = false; + await richCreateMessage(fields, [], identity); + + let expectedBody = longMultibyteLineJapanese + " " + newline + newline; + + checkDraftHeadersAndBody( + { + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "base64", + }, + expectedBody + ); + + // Again, but this time not flowed. + fields.body = htmlMessage; + Services.prefs.setBoolPref("mailnews.send_plaintext_flowed", false); + + await richCreateMessage(fields, [], identity); + checkDraftHeadersAndBody( + { + "Content-Type": "text/plain; charset=UTF-8", + "Content-Transfer-Encoding": "base64", + }, + expectedBody.replace(/ /g, "") // No spaces expected this time. + ); +} + +var tests = [testBodyWithLongLine]; + +function run_test() { + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/compose/test/unit/test_mailTelemetry.js b/comm/mailnews/compose/test/unit/test_mailTelemetry.js new file mode 100644 index 0000000000..28959d508f --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_mailTelemetry.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test telemetry related to mails sent. + */ + +let { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +let server; + +let kIdentityMail = "identity@foo.invalid"; +let kSender = "from@foo.invalid"; +let kTo = "to@foo.invalid"; + +const NUM_MAILS = 3; + +let deliveryListener = { + count: 0, + OnStartRunningUrl() {}, + OnStopRunningUrl() { + if (++this.count == NUM_MAILS) { + let scalars = TelemetryTestUtils.getProcessScalars("parent"); + Assert.equal( + scalars["tb.mails.sent"], + NUM_MAILS, + "Count of mails sent must be correct." + ); + } + }, +}; + +/** + * Check that we're counting mails sent. + */ +add_task(async function test_mails_sent() { + Services.telemetry.clearScalars(); + + server = setupServerDaemon(); + registerCleanupFunction(() => { + server.stop(); + }); + + // Test file + let testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server + server.start(); + let smtpServer = getBasicSmtpServer(server.port); + let identity = getSmtpIdentity(kIdentityMail, smtpServer); + + for (let i = 0; i < NUM_MAILS; i++) { + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + deliveryListener, + null, + null, + false, + "", + {}, + {} + ); + } + } catch (e) { + do_throw(e); + } +}); diff --git a/comm/mailnews/compose/test/unit/test_mailtoURL.js b/comm/mailnews/compose/test/unit/test_mailtoURL.js new file mode 100644 index 0000000000..a793c99974 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_mailtoURL.js @@ -0,0 +1,810 @@ +/* + * Test suite for mailto: URLs + */ + +var COMPOSE_HTML = Ci.nsIMsgCompFormat.HTML; +var COMPOSE_DEFAULT = Ci.nsIMsgCompFormat.Default; + +function run_test() { + function test(aTest) { + var uri = Services.io.newURI(aTest.url); + uri = uri.QueryInterface(Ci.nsIMailtoUrl); + + var to = {}, + cc = {}, + bcc = {}, + subject = {}, + body = {}, + html = {}, + reference = {}, + newsgroup = {}, + composeformat = {}; + uri.getMessageContents( + to, + cc, + bcc, + subject, + body, + html, + reference, + newsgroup, + composeformat + ); + Assert.equal(aTest.to, to.value); + Assert.equal(aTest.cc, cc.value); + Assert.equal(aTest.bcc, bcc.value); + Assert.equal(aTest.subject, subject.value); + Assert.equal(aTest.body, body.value); + Assert.equal(aTest.html, html.value); + Assert.equal(aTest.reference, reference.value); + Assert.equal(aTest.newsgroup, newsgroup.value); + Assert.equal(aTest.composeformat, composeformat.value); + Assert.equal(aTest.from, uri.fromPart); + Assert.equal(aTest.followupto, uri.followUpToPart); + Assert.equal(aTest.organization, uri.organizationPart); + Assert.equal(aTest.replyto, uri.replyToPart); + Assert.equal(aTest.priority, uri.priorityPart); + Assert.equal(aTest.newshost, uri.newsHostPart); + Assert.ok(uri.equals(uri)); + } + + for (var i = 0; i < tests.length; i++) { + test(tests[i]); + } + + // Test cloning reparses the url by checking the to field. + let uri = Services.io.newURI(tests[0].url).QueryInterface(Ci.nsIMailtoUrl); + var to = {}, + cc = {}, + bcc = {}, + subject = {}, + body = {}, + html = {}, + reference = {}, + newsgroup = {}, + composeformat = {}; + uri.getMessageContents( + to, + cc, + bcc, + subject, + body, + html, + reference, + newsgroup, + composeformat + ); + Assert.equal(to.value, tests[0].to); +} + +var tests = [ + { + url: "mailto:one@example.com", + to: "one@example.com", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:two@example.com?", + to: "two@example.com", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + /* the heirarchical-part address shouldn't be mime-decoded */ + { + url: "mailto:%3D%3FUTF-8%3FQ%3Fthree%3F%3D@example.com", + to: "=?UTF-8?Q?three?=@example.com", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + /* a to=address should be mime-decoded */ + { + url: "mailto:?to=%3D%3FUTF-8%3FQ%3Ffour%3F%3D@example.com", + to: "four@example.com", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:fivea@example.com?to=%3D%3FUTF-8%3FQ%3Ffiveb%3F%3D@example.com", + to: "fivea@example.com, fiveb@example.com", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:sixa@example.com?to=sixb@example.com&to=sixc@example.com", + to: "sixa@example.com, sixb@example.com, sixc@example.com", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?cc=seven@example.com", + to: "", + cc: "seven@example.com", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?cc=%3D%3FUTF-8%3FQ%3Feight%3F%3D@example.com", + to: "", + cc: "eight@example.com", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?bcc=nine@example.com", + to: "", + cc: "", + bcc: "nine@example.com", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?bcc=%3D%3FUTF-8%3FQ%3Ften%3F%3D@example.com", + to: "", + cc: "", + bcc: "ten@example.com", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?subject=foo", + to: "", + cc: "", + bcc: "", + subject: "foo", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?subject=%62%61%72", + to: "", + cc: "", + bcc: "", + subject: "bar", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?subject=%3D%3Futf-8%3FQ%3F%3DC2%3DA1encoded_subject%21%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "¡encoded subject!", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?body=one%20body", + to: "", + cc: "", + bcc: "", + subject: "", + body: "one body", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?body=two%20bodies&body=two%20lines", + to: "", + cc: "", + bcc: "", + subject: "", + body: "two bodies\ntwo lines", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?html-part=html%20part", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "html part", + reference: "", + newsgroup: "", + composeformat: COMPOSE_HTML, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?html-body=html%20body", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "html body", + reference: "", + newsgroup: "", + composeformat: COMPOSE_HTML, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?html-part=html%20part&html-body=html-body%20trumps%20earlier%20html-part", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "html-body trumps earlier html-part", + reference: "", + newsgroup: "", + composeformat: COMPOSE_HTML, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?references=%3Cref1%40example.com%3E", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "<ref1@example.com>", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?in-reply-to=%3Crepl1%40example.com%3E", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "<repl1@example.com>", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: + "mailto:?references=%3Cref2%40example.com%3E" + + "&in-reply-to=%3Crepl2%40example.com%3E", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "<ref2@example.com> <repl2@example.com>", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: + "mailto:?references=%3Cref3%40example.com%3E%20%3Crepl3%40example.com%3E" + + "&in-reply-to=%3Crepl3%40example.com%3E", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "<ref3@example.com> <repl3@example.com>", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?newsgroups=mozilla.dev.apps.thunderbird", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "mozilla.dev.apps.thunderbird", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?newsgroups=%3D%3FUTF-8%3FQ%3Fmozilla.test.multimedia%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "mozilla.test.multimedia", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?from=notlikely@example.com", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "notlikely@example.com", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?from=%3D%3FUTF-8%3FQ%3Fme@example.com%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "me@example.com", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?followup-to=mozilla.dev.planning", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "mozilla.dev.planning", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?followup-to=%3D%3FUTF-8%3FQ%3Fmozilla.test%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "mozilla.test", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?organization=very%20little", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "very little", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?organization=%3D%3FUTF-8%3FQ%3Fmicroscopic%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "microscopic", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?reply-to=notme@example.com", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "notme@example.com", + priority: "", + newshost: "", + }, + { + url: "mailto:?reply-to=%3D%3FUTF-8%3FB%3Fw4VrZQ%3D%3D%3F%3D%20%3Cake@example.org%3E", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "Åke <ake@example.org>", + priority: "", + newshost: "", + }, + { + url: "mailto:?priority=1%20(People%20Are%20Dying!!1!)", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "1 (People Are Dying!!1!)", + newshost: "", + }, + { + url: "mailto:?priority=%3D%3FUTF-8%3FQ%3F4%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "4", + newshost: "", + }, + { + url: "mailto:?newshost=news.mozilla.org", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "news.mozilla.org", + }, + { + url: "mailto:?newshost=%3D%3FUTF-8%3FQ%3Fnews.example.org%3F%3D", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "news.example.org", + }, + { + url: "mailto:?%74%4F=to&%73%55%62%4A%65%43%74=subject&%62%4F%64%59=body&%63%43=cc&%62%43%63=bcc", + to: "to", + cc: "cc", + bcc: "bcc", + subject: "subject", + body: "body", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:to1?%74%4F=to2&to=to3&subject=&%73%55%62%4A%65%43%74=subject&%62%4F%64%59=line1&body=line2&%63%43=cc1&cc=cc2&%62%43%63=bcc1&bcc=bcc2", + to: "to1, to2, to3", + cc: "cc1, cc2", + bcc: "bcc1, bcc2", + subject: "subject", + body: "line1\nline2", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: "mailto:?nto=1&nsubject=2&nbody=3&ncc=4&nbcc=5", + to: "", + cc: "", + bcc: "", + subject: "", + body: "", + html: "", + reference: "", + newsgroup: "", + composeformat: COMPOSE_DEFAULT, + from: "", + followupto: "", + organization: "", + replyto: "", + priority: "", + newshost: "", + }, + { + url: + "mailto:%CE%B1?cc=%CE%B2&bcc=%CE%B3&subject=%CE%B4&body=%CE%B5" + + "&html-body=%CE%BE&newsgroups=%CE%B6&from=%CE%B7&followup-to=%CE%B8" + + "&organization=%CE%B9&reply-to=%CE%BA&priority=%CE%BB&newshost=%CE%BC", + to: "α", + cc: "β", + bcc: "γ", + subject: "δ", + body: "ε", + html: "ξ", + reference: "", // we expect this field to be ASCII-only + newsgroup: "ζ", + composeformat: COMPOSE_HTML, + from: "η", + followupto: "θ", + organization: "ι", + replyto: "κ", + priority: "λ", + newshost: "μ", + }, +]; diff --git a/comm/mailnews/compose/test/unit/test_messageBody.js b/comm/mailnews/compose/test/unit/test_messageBody.js new file mode 100644 index 0000000000..14c44b59f8 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_messageBody.js @@ -0,0 +1,206 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/** + * Test suite for message body. + */ + +localAccountUtils.loadLocalMailAccount(); + +/** + * Test trailing whitespace is QP encoded. + */ +add_task(async function testQP() { + // Together with fields.forceMsgEncoding, force quote-printable encoding. + Services.prefs.setBoolPref("mail.strictly_mime", true); + + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + let CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields + ); + + // Test QP works for ascii text. + + let fields = new CompFields(); + fields.forceMsgEncoding = true; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test QP encoding for trailing whitespace"; + fields.body = "A line with trailing whitespace\t "; + await richCreateMessage(fields, [], identity); + + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + Assert.ok( + msgData.includes("A line with trailing whitespace\t=20"), + "QP for ascii should work" + ); + + // Test QP works for non-ascii text. + + fields = new CompFields(); + fields.forceMsgEncoding = true; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test QP encoding for non-ascii and trailing tab"; + fields.body = "記: base64 is used if unprintable > 10% \t"; + await richCreateMessage(fields, [], identity); + + msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + Assert.ok( + msgData.includes("=E8=A8=98: base64 is used if unprintable > 10% =09"), + "QP for non-ascii should work" + ); + + // Test leading space is preserved. + + fields = new CompFields(); + fields.forceMsgEncoding = true; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Leading space is valid in a quoted printable message"; + fields.body = "123456789" + " 123456789".repeat(6) + "1234 56789"; + await richCreateMessage(fields, [], identity); + + msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + let endOfHeaders = msgData.indexOf("\r\n\r\n"); + let body = msgData.slice(endOfHeaders + 4); + + Assert.equal( + body.trimRight("\r\n"), + "123456789 123456789 123456789 123456789 123456789 123456789 1234567891234=\r\n 56789" + ); + + Services.prefs.clearUserPref("mail.strictly_mime"); +}); + +/** + * Test QP is not used together with format=flowed. + */ +add_task(async function testNoQPWithFormatFlowed() { + // Together with fields.forceMsgEncoding, force quote-printable encoding. + Services.prefs.setBoolPref("mail.strictly_mime", true); + + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.forceMsgEncoding = true; + fields.forcePlainText = true; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test QP encoding for trailing whitespace"; + fields.body = "A line with trailing whitespace\t "; + await richCreateMessage(fields, [], identity); + + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + Assert.ok( + msgData.includes( + "Content-Type: text/plain; charset=UTF-8; format=flowed\r\nContent-Transfer-Encoding: base64" + ), + "format=flowed should be used" + ); + Assert.ok( + !msgData.includes("quoted-printable"), + "quoted-printable should not be used" + ); + + Services.prefs.clearUserPref("mail.strictly_mime"); +}); + +/** + * Test plain text body is wrapped correctly with different mailnews.wraplength + * pref value. + */ +add_task(async function testWrapLength() { + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + let CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields + ); + + let word = "abcd "; + let body = word.repeat(20); + + let fields = new CompFields(); + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test text wrapping"; + fields.body = `<html><body>${body}</body></html>`; + fields.forcePlainText = true; + await richCreateMessage(fields, [], identity); + + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + Assert.equal( + getMessageBody(msgData), + // Default wrap length is 72. + word.repeat(14) + "\r\n" + word.repeat(6).trim(), + "Text wraps at 72 by default" + ); + + // 0 means no wrap. + Services.prefs.setIntPref("mailnews.wraplength", 0); + + await richCreateMessage(fields, [], identity); + + msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + Assert.equal( + getMessageBody(msgData), + body.trim(), + "Should not wrap when wraplength is 0" + ); + + Services.prefs.clearUserPref("mailnews.wraplength"); +}); + +/** + * Test handling of trailing NBSP. + */ +add_task(async function testNBSP() { + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.subject = "Test text wrapping"; + // The character after `test` is NBSP. + fields.body = "<html><body>åäö test <br></body></html>"; + fields.forcePlainText = true; + await richCreateMessage(fields, [], identity); + + let msgData = mailTestUtils.loadMessageToUTF16String( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + Assert.equal( + getMessageBody(msgData), + "åäö test", + "Trailing NBSP should be removed" + ); +}); diff --git a/comm/mailnews/compose/test/unit/test_messageHeaders.js b/comm/mailnews/compose/test/unit/test_messageHeaders.js new file mode 100644 index 0000000000..58765e219f --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_messageHeaders.js @@ -0,0 +1,812 @@ +/* + * Test suite for ensuring that the headers of messages are set properly. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +var CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields +); + +function makeAttachment(opts = {}) { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + for (let key in opts) { + attachment[key] = opts[key]; + } + return attachment; +} + +function sendMessage(fieldParams, identity, opts = {}, attachments = []) { + // Initialize compose fields + let fields = new CompFields(); + for (let key in fieldParams) { + fields[key] = fieldParams[key]; + } + for (let attachment of attachments) { + fields.addAttachment(attachment); + } + + // Initialize compose params + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + for (let key in opts) { + params[key] = opts[key]; + } + + // Send the message + let msgCompose = MailServices.compose.initCompose(params); + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let promise = new Promise((resolve, reject) => { + progressListener.resolve = resolve; + progressListener.reject = reject; + }); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgDeliverNow, + identity, + "", + null, + progress + ); + return promise; +} + +function checkDraftHeaders(expectedHeaders, partNum = "") { + let msgData = mailTestUtils.loadMessageToString( + gDraftFolder, + mailTestUtils.firstMsgHdr(gDraftFolder) + ); + checkMessageHeaders(msgData, expectedHeaders, partNum); +} + +function checkMessageHeaders(msgData, expectedHeaders, partNum = "") { + let seen = false; + let handler = { + startPart(part, headers) { + if (part != partNum) { + return; + } + seen = true; + for (let header in expectedHeaders) { + let expected = expectedHeaders[header]; + if (expected === undefined) { + Assert.ok( + !headers.has(header), + `Should not have header named "${header}"` + ); + } else { + let value = headers.getRawHeader(header); + Assert.equal( + value && value.length, + 1, + `Should have exactly one header named "${header}"` + ); + value[0] = value[0].replace(/boundary=[^;]*(;|$)/, "boundary=."); + Assert.equal(value[0], expected); + } + } + }, + }; + MimeParser.parseSync(msgData, handler, { + onerror(e) { + throw e; + }, + }); + Assert.ok(seen); +} + +async function testEnvelope() { + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + identity.fullName = "Me"; + identity.organization = "World Destruction Committee"; + fields.from = "Nobody <nobody@tinderbox.invalid>"; + fields.to = "Nobody <nobody@tinderbox.invalid>"; + fields.cc = "Alex <alex@tinderbox.invalid>"; + fields.bcc = "Boris <boris@tinderbox.invalid>"; + fields.replyTo = "Charles <charles@tinderbox.invalid>"; + fields.organization = "World Salvation Committee"; + fields.subject = "This is an obscure reference"; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + // As of bug 87987, the identity does not override the from header. + From: "Nobody <nobody@tinderbox.invalid>", + // The identity should override the organization field here. + Organization: "World Destruction Committee", + To: "Nobody <nobody@tinderbox.invalid>", + Cc: "Alex <alex@tinderbox.invalid>", + Bcc: "Boris <boris@tinderbox.invalid>", + "Reply-To": "Charles <charles@tinderbox.invalid>", + Subject: "This is an obscure reference", + }); +} + +async function testI18NEnvelope() { + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + identity.fullName = "ケツァルコアトル"; + identity.organization = "Comité de la destruction du monde"; + fields.to = "Émile <nobody@tinderbox.invalid>"; + fields.cc = "André Chopin <alex@tinderbox.invalid>"; + fields.bcc = "Étienne <boris@tinderbox.invalid>"; + fields.replyTo = "Frédéric <charles@tinderbox.invalid>"; + fields.subject = "Ceci n'est pas un référence obscure"; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + From: "=?UTF-8?B?44Kx44OE44Kh44Or44Kz44Ki44OI44Or?= <from@tinderbox.invalid>", + Organization: "=?UTF-8?Q?Comit=C3=A9_de_la_destruction_du_monde?=", + To: "=?UTF-8?B?w4ltaWxl?= <nobody@tinderbox.invalid>", + Cc: "=?UTF-8?Q?Andr=C3=A9_Chopin?= <alex@tinderbox.invalid>", + Bcc: "=?UTF-8?Q?=C3=89tienne?= <boris@tinderbox.invalid>", + "Reply-To": "=?UTF-8?B?RnLDqWTDqXJpYw==?= <charles@tinderbox.invalid>", + Subject: "=?UTF-8?Q?Ceci_n=27est_pas_un_r=C3=A9f=C3=A9rence_obscure?=", + }); +} + +async function testIDNEnvelope() { + let fields = new CompFields(); + let domain = "ケツァルコアトル.invalid"; + // We match against rawHeaderText, so we need to encode the string as a binary + // string instead of a unicode string. + let utf8Domain = String.fromCharCode.apply( + undefined, + new TextEncoder("UTF-8").encode(domain) + ); + // Bug 1034658: nsIMsgIdentity doesn't like IDN in its email addresses. + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + fields.to = "Nobody <nobody@" + domain + ">"; + fields.cc = "Alex <alex@" + domain + ">"; + fields.bcc = "Boris <boris@" + domain + ">"; + fields.replyTo = "Charles <charles@" + domain + ">"; + fields.subject = "This is an obscure reference"; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + // The identity sets the from field here. + From: "from@tinderbox.invalid", + To: "Nobody <nobody@" + utf8Domain + ">", + Cc: "Alex <alex@" + utf8Domain + ">", + Bcc: "Boris <boris@" + utf8Domain + ">", + "Reply-To": "Charles <charles@" + utf8Domain + ">", + Subject: "This is an obscure reference", + }); +} + +async function testDraftInfo() { + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + FCC: identity.fccFolder, + "X-Identity-Key": identity.key, + "X-Mozilla-Draft-Info": + "internal/draft; " + + "vcard=0; receipt=0; DSN=0; uuencode=0; attachmentreminder=0; deliveryformat=4", + }); + + fields.attachVCard = true; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Mozilla-Draft-Info": + "internal/draft; " + + "vcard=1; receipt=0; DSN=0; uuencode=0; attachmentreminder=0; deliveryformat=4", + }); + + fields.returnReceipt = true; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Mozilla-Draft-Info": + "internal/draft; " + + "vcard=1; receipt=1; DSN=0; uuencode=0; attachmentreminder=0; deliveryformat=4", + }); + + fields.DSN = true; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Mozilla-Draft-Info": + "internal/draft; " + + "vcard=1; receipt=1; DSN=1; uuencode=0; attachmentreminder=0; deliveryformat=4", + }); + + fields.attachmentReminder = true; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Mozilla-Draft-Info": + "internal/draft; " + + "vcard=1; receipt=1; DSN=1; uuencode=0; attachmentreminder=1; deliveryformat=4", + }); + + fields.deliveryFormat = Ci.nsIMsgCompSendFormat.Both; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Mozilla-Draft-Info": + "internal/draft; " + + "vcard=1; receipt=1; DSN=1; uuencode=0; attachmentreminder=1; deliveryformat=3", + }); +} + +async function testOtherHeadersAgentParam(sendAgent, minimalAgent) { + Services.prefs.setBoolPref("mailnews.headers.sendUserAgent", sendAgent); + if (sendAgent) { + Services.prefs.setBoolPref( + "mailnews.headers.useMinimalUserAgent", + minimalAgent + ); + } + + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + fields.priority = "high"; + fields.references = "<fake@tinderbox.invalid> <more@test.invalid>"; + fields.setHeader("X-Fake-Header", "124"); + let before = Date.now(); + let msgHdr = await richCreateMessage(fields, [], identity); + let after = Date.now(); + let msgData = mailTestUtils.loadMessageToString(msgHdr.folder, msgHdr); + let expectedAgent = undefined; // !sendAgent + if (sendAgent) { + if (minimalAgent) { + expectedAgent = Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandFullName"); + } else { + expectedAgent = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler).userAgent; + } + } + checkMessageHeaders(msgData, { + "Mime-Version": "1.0", + "User-Agent": expectedAgent, + "X-Priority": "2 (High)", + References: "<fake@tinderbox.invalid> <more@test.invalid>", + "In-Reply-To": "<more@test.invalid>", + "X-Fake-Header": "124", + }); + + // Check headers with dynamic content + let headers = MimeParser.extractHeaders(msgData); + Assert.ok(headers.has("Message-Id")); + Assert.ok( + headers.getRawHeader("Message-Id")[0].endsWith("@tinderbox.invalid>") + ); + // This is a very special crafted check. We don't know when the message was + // actually created, but we have bounds on it, from above. From + // experimentation, there are a few ways you can create dates that Date.parse + // can't handle (specifically related to how 2-digit years). However, the + // optimal RFC 5322 form is supported by Date.parse. If Date.parse fails, we + // have a form that we shouldn't be using anyways. + let date = new Date(headers.getRawHeader("Date")[0]); + // If we have clock skew within the test, then our results are going to be + // meaningless. Hopefully, this is only rarely the case. + if (before > after) { + info("Clock skew detected, skipping date check"); + } else { + // In case this all took place within one second, remove sub-millisecond + // timing (Date headers only carry second-level precision). + before = before - (before % 1000); + after = after - (after % 1000); + info(before + " <= " + date + " <= " + after + "?"); + Assert.ok(before <= date && date <= after); + } + + // We truncate too-long References. Check this. + let references = []; + for (let i = 0; i < 100; i++) { + references.push("<" + i + "@test.invalid>"); + } + let expected = references.slice(47); + expected.unshift(references[0]); + fields.references = references.join(" "); + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + References: expected.join(" "), + "In-Reply-To": references[references.length - 1], + }); +} + +/** + * Tests that the domain for the Message-Id header defaults to the domain of the + * identity's address. + */ +async function testMessageIdUseIdentityAddress() { + const expectedMessageIdHostname = "tinderbox.test"; + + const identity = getSmtpIdentity( + `from@${expectedMessageIdHostname}`, + getBasicSmtpServer() + ); + + await createMsgAndCompareMessageId(identity, null, expectedMessageIdHostname); +} + +/** + * Tests that if a custom address (with a custom domain) is used when composing a + * message, the domain in this address takes precendence over the domain of the + * identity's address to generate the value for the Message-Id header. + */ +async function testMessageIdUseFromDomain() { + const expectedMessageIdHostname = "another-tinderbox.test"; + + const identity = getSmtpIdentity("from@tinderbox.test", getBasicSmtpServer()); + + // Set the From header to an address that uses a different domain than + // the identity. + const fields = new CompFields(); + fields.from = `Nobody <nobody@${expectedMessageIdHostname}>`; + + await createMsgAndCompareMessageId( + identity, + fields, + expectedMessageIdHostname + ); +} + +/** + * Tests that if the identity has a "FQDN" attribute, it takes precedence to use as the + * domain for the Message-Id header over any other domain or address. + */ +async function testMessageIdUseIdentityAttribute() { + const expectedMessageIdHostname = "my-custom-fqdn.test"; + + const identity = getSmtpIdentity("from@tinderbox.test", getBasicSmtpServer()); + identity.setCharAttribute("FQDN", expectedMessageIdHostname); + + // Set the From header to an address that uses a different domain than + // the identity. + const fields = new CompFields(); + fields.from = "Nobody <nobody@another-tinderbox.test>"; + + await createMsgAndCompareMessageId( + identity, + fields, + expectedMessageIdHostname + ); +} + +/** + * Util function to create a message using the given identity and fields, + * and test that the message ID that was generated for it has the correct + * host name. + * + * @param {nsIMsgIdentity} identity - The identity to use to create the message. + * @param {?nsIMsgCompFields} fields - The compose fields to use. If not provided, + * default fields are used. + * @param {string} expectedMessageIdHostname - The expected host name of the + * Message-Id header. + */ +async function createMsgAndCompareMessageId( + identity, + fields, + expectedMessageIdHostname +) { + if (!fields) { + fields = new CompFields(); + } + + let msgHdr = await richCreateMessage(fields, [], identity); + let msgData = mailTestUtils.loadMessageToString(msgHdr.folder, msgHdr); + let headers = MimeParser.extractHeaders(msgData); + + // As of bug 1727181, the identity does not override the message-id header. + Assert.ok(headers.has("Message-Id"), "the message has a Message-Id header"); + Assert.ok( + headers + .getRawHeader("Message-Id")[0] + .endsWith(`@${expectedMessageIdHostname}>`), + `the hostname for the Message-Id header should be ${expectedMessageIdHostname}` + ); +} + +async function testOtherHeadersFullAgent() { + await testOtherHeadersAgentParam(true, false); +} + +async function testOtherHeadersMinimalAgent() { + await testOtherHeadersAgentParam(true, true); +} + +async function testOtherHeadersNoAgent() { + await testOtherHeadersAgentParam(false, undefined); +} + +async function testNewsgroups() { + let fields = new CompFields(); + let nntpServer = localAccountUtils.create_incoming_server( + "nntp", + 534, + "", + "" + ); + nntpServer + .QueryInterface(Ci.nsINntpIncomingServer) + .subscribeToNewsgroup("mozilla.test"); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + fields.newsgroups = "mozilla.test, mozilla.test.multimedia"; + fields.followupTo = "mozilla.test"; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + // The identity should override the compose fields here. + Newsgroups: "mozilla.test,mozilla.test.multimedia", + "Followup-To": "mozilla.test", + "X-Mozilla-News-Host": "localhost", + }); +} + +async function testSendHeaders() { + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + identity.setCharAttribute("headers", "bah,humbug"); + identity.setCharAttribute( + "header.bah", + "X-Custom-1: A header value: with a colon" + ); + identity.setUnicharAttribute("header.humbug", "X-Custom-2: Enchanté"); + identity.setCharAttribute("subscribed_mailing_lists", "list@test.invalid"); + identity.setCharAttribute( + "replyto_mangling_mailing_lists", + "replyto@test.invalid" + ); + fields.to = "list@test.invalid"; + fields.cc = "not-list@test.invalid"; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Custom-1": "A header value: with a colon", + "X-Custom-2": "=?UTF-8?B?RW5jaGFudMOp?=", + "Mail-Followup-To": "list@test.invalid, not-list@test.invalid", + "Mail-Reply-To": undefined, + }); + + // Don't set the M-F-T header if there's no list. + fields.to = "replyto@test.invalid"; + fields.cc = ""; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "X-Custom-1": "A header value: with a colon", + "X-Custom-2": "=?UTF-8?B?RW5jaGFudMOp?=", + "Mail-Reply-To": "from@tinderbox.invalid", + "Mail-Followup-To": undefined, + }); +} + +async function testContentHeaders() { + // Disable RFC 2047 fallback + Services.prefs.setIntPref("mail.strictly_mime.parm_folding", 2); + let fields = new CompFields(); + fields.body = "A body"; + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "7bit", + }); + + // non-ASCII body should be 8-bit... + fields.body = "Archæologist"; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "8bit", + }); + + // Attachments + fields.body = ""; + let plainAttachment = makeAttachment({ + url: "data:text/plain,oïl", + name: "attachment.txt", + }); + let plainAttachmentHeaders = { + "Content-Type": "text/plain; charset=UTF-8", + "Content-Transfer-Encoding": "base64", + "Content-Disposition": 'attachment; filename="attachment.txt"', + }; + await richCreateMessage(fields, [plainAttachment], identity); + checkDraftHeaders( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "7bit", + }, + "1" + ); + checkDraftHeaders(plainAttachmentHeaders, "2"); + + plainAttachment.name = "oïl.txt"; + plainAttachmentHeaders["Content-Disposition"] = + "attachment; filename*=UTF-8''%6F%C3%AF%6C%2E%74%78%74"; + await richCreateMessage(fields, [plainAttachment], identity); + checkDraftHeaders(plainAttachmentHeaders, "2"); + + plainAttachment.name = "\ud83d\udca9.txt"; + plainAttachmentHeaders["Content-Disposition"] = + "attachment; filename*=UTF-8''%F0%9F%92%A9%2E%74%78%74"; + await richCreateMessage(fields, [plainAttachment], identity); + checkDraftHeaders(plainAttachmentHeaders, "2"); + + let httpAttachment = makeAttachment({ + url: "data:text/html,<html></html>", + name: "attachment.html", + }); + let httpAttachmentHeaders = { + "Content-Type": "text/html; charset=UTF-8", + "Content-Disposition": 'attachment; filename="attachment.html"', + "Content-Location": "data:text/html,<html></html>", + }; + await richCreateMessage(fields, [httpAttachment], identity); + checkDraftHeaders( + { + "Content-Location": undefined, + }, + "1" + ); + checkDraftHeaders(httpAttachmentHeaders, "2"); + + let cloudAttachment = makeAttachment({ + url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec, + sendViaCloud: true, + htmlAnnotation: + "<html><body>This is an html placeholder file.</body></html>", + cloudFileAccountKey: "akey", + cloudPartHeaderData: "0123456789ABCDE", + name: "attachment.html", + contentLocation: "http://localhost.invalid/", + }); + let cloudAttachmentHeaders = { + "Content-Type": "text/html; charset=utf-8", + "X-Mozilla-Cloud-Part": + "cloudFile; " + + "url=http://localhost.invalid/; " + + "provider=akey; " + + 'data="0123456789ABCDE"', + }; + await richCreateMessage(fields, [cloudAttachment], identity); + checkDraftHeaders(cloudAttachmentHeaders, "2"); + + // Cloud attachment with non-ascii file name. + cloudAttachment = makeAttachment({ + url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec, + sendViaCloud: true, + htmlAnnotation: + "<html><body>This is an html placeholder file.</body></html>", + cloudFileAccountKey: "akey", + cloudPartHeaderData: "0123456789ABCDE", + name: "ファイル.txt", + contentLocation: "http://localhost.invalid/", + }); + cloudAttachmentHeaders = { + "Content-Type": "text/html; charset=utf-8", + "X-Mozilla-Cloud-Part": + "cloudFile; " + + "url=http://localhost.invalid/; " + + "provider=akey; " + + 'data="0123456789ABCDE"', + }; + await richCreateMessage(fields, [cloudAttachment], identity); + checkDraftHeaders(cloudAttachmentHeaders, "2"); + + // Some multipart/alternative tests. + fields.body = "Some text"; + fields.forcePlainText = false; + fields.useMultipartAlternative = true; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "Content-Type": "multipart/alternative; boundary=.", + }); + checkDraftHeaders( + { + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "7bit", + }, + "1" + ); + checkDraftHeaders( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "7bit", + }, + "2" + ); + + // multipart/mixed + // + multipart/alternative + // + text/plain + // + text/html + // + text/plain attachment + await richCreateMessage(fields, [plainAttachment], identity); + checkDraftHeaders({ + "Content-Type": "multipart/mixed; boundary=.", + }); + checkDraftHeaders( + { + "Content-Type": "multipart/alternative; boundary=.", + }, + "1" + ); + checkDraftHeaders( + { + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "7bit", + }, + "1.1" + ); + checkDraftHeaders( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "7bit", + }, + "1.2" + ); + checkDraftHeaders(plainAttachmentHeaders, "2"); + + // Three attachments, and a multipart/alternative. Oh the humanity! + await richCreateMessage( + fields, + [plainAttachment, httpAttachment, cloudAttachment], + identity + ); + checkDraftHeaders({ + "Content-Type": "multipart/mixed; boundary=.", + }); + checkDraftHeaders( + { + "Content-Type": "multipart/alternative; boundary=.", + }, + "1" + ); + checkDraftHeaders( + { + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "7bit", + }, + "1.1" + ); + checkDraftHeaders( + { + "Content-Type": "text/html; charset=UTF-8", + "Content-Transfer-Encoding": "7bit", + }, + "1.2" + ); + checkDraftHeaders(plainAttachmentHeaders, "2"); + checkDraftHeaders(httpAttachmentHeaders, "3"); + checkDraftHeaders(cloudAttachmentHeaders, "4"); + + // Test a request for plain text with text/html. + fields.forcePlainText = true; + fields.useMultipartAlternative = false; + await richCreateMessage(fields, [], identity); + checkDraftHeaders({ + "Content-Type": "text/plain; charset=UTF-8; format=flowed", + "Content-Transfer-Encoding": "7bit", + }); +} + +async function testSentMessage() { + let server = setupServerDaemon(); + let daemon = server._daemon; + server.start(); + try { + let localserver = getBasicSmtpServer(server.port); + let identity = getSmtpIdentity("test@tinderbox.invalid", localserver); + await sendMessage( + { + to: "Nobody <nobody@tinderbox.invalid>", + cc: "Alex <alex@tinderbox.invalid>", + bcc: "Boris <boris@tinderbox.invalid>", + replyTo: "Charles <charles@tinderbox.invalid>", + }, + identity, + {}, + [] + ); + checkMessageHeaders(daemon.post, { + From: "test@tinderbox.invalid", + To: "Nobody <nobody@tinderbox.invalid>", + Cc: "Alex <alex@tinderbox.invalid>", + Bcc: undefined, + "Reply-To": "Charles <charles@tinderbox.invalid>", + "X-Mozilla-Status": undefined, + "X-Mozilla-Keys": undefined, + "X-Mozilla-Draft-Info": undefined, + Fcc: undefined, + }); + server.resetTest(); + await sendMessage({ bcc: "Somebody <test@tinderbox.invalid" }, identity); + checkMessageHeaders(daemon.post, { + To: "undisclosed-recipients: ;", + }); + server.resetTest(); + await sendMessage( + { + to: "Somebody <test@tinderbox.invalid>", + returnReceipt: true, + receiptHeaderType: Ci.nsIMsgMdnGenerator.eDntRrtType, + }, + identity + ); + checkMessageHeaders(daemon.post, { + "Disposition-Notification-To": "test@tinderbox.invalid", + "Return-Receipt-To": "test@tinderbox.invalid", + }); + server.resetTest(); + let cloudAttachment = makeAttachment({ + url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec, + sendViaCloud: true, + htmlAnnotation: + "<html><body>This is an html placeholder file.</body></html>", + cloudFileAccountKey: "akey", + cloudPartHeaderData: "0123456789ABCDE", + name: "attachment.html", + contentLocation: "http://localhost.invalid/", + }); + await sendMessage({ to: "test@tinderbox.invalid" }, identity, {}, [ + cloudAttachment, + ]); + checkMessageHeaders( + daemon.post, + { + "Content-Type": "text/html; charset=utf-8", + "X-Mozilla-Cloud-Part": "cloudFile; url=http://localhost.invalid/", + }, + "2" + ); + } finally { + server.stop(); + } +} + +var tests = [ + testEnvelope, + testI18NEnvelope, + testIDNEnvelope, + testDraftInfo, + testOtherHeadersFullAgent, + testOtherHeadersMinimalAgent, + testOtherHeadersNoAgent, + testNewsgroups, + testSendHeaders, + testContentHeaders, + testSentMessage, + testMessageIdUseIdentityAddress, + testMessageIdUseFromDomain, + testMessageIdUseIdentityAttribute, +]; + +function run_test() { + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js b/comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js new file mode 100644 index 0000000000..69c1b753b6 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_nsIMsgCompFields.js @@ -0,0 +1,62 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// Test that nsIMsgCompFields works properly + +var nsMsgCompFields = Components.Constructor( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields +); + +function check_headers(enumerator, container) { + let checkValues = new Set(container.map(header => header.toLowerCase())); + for (let value of enumerator) { + value = value.toLowerCase(); + Assert.ok(checkValues.has(value)); + checkValues.delete(value); + } + Assert.equal(checkValues.size, 0); +} + +function run_test() { + let fields = new nsMsgCompFields(); + Assert.ok(fields instanceof Ci.nsIMsgCompFields); + Assert.ok(fields instanceof Ci.msgIStructuredHeaders); + Assert.ok(fields instanceof Ci.msgIWritableStructuredHeaders); + check_headers(fields.headerNames, []); + Assert.ok(!fields.hasRecipients); + + // Try some basic headers + fields.setHeader("From", [{ name: "", email: "a@test.invalid" }]); + let from = fields.getHeader("from"); + Assert.equal(from.length, 1); + Assert.equal(from[0].email, "a@test.invalid"); + check_headers(fields.headerNames, ["From"]); + Assert.ok(!fields.hasRecipients); + + // Add a To header + fields.setHeader("To", [{ name: "", email: "b@test.invalid" }]); + check_headers(fields.headerNames, ["From", "To"]); + Assert.ok(fields.hasRecipients); + + // Delete a header... + fields.deleteHeader("from"); + Assert.equal(fields.getHeader("From"), undefined); + check_headers(fields.headerNames, ["To"]); + + // Subject should work and not convert to RFC 2047. + fields.subject = "\u79c1\u306f\u4ef6\u540d\u5348\u524d"; + Assert.equal(fields.subject, "\u79c1\u306f\u4ef6\u540d\u5348\u524d"); + Assert.equal( + fields.getHeader("Subject"), + "\u79c1\u306f\u4ef6\u540d\u5348\u524d" + ); + + // Check header synchronization. + fields.from = "a@test.invalid"; + Assert.equal(fields.from, "a@test.invalid"); + Assert.equal(fields.getHeader("From")[0].email, "a@test.invalid"); + fields.from = null; + Assert.equal(fields.getHeader("From"), undefined); +} diff --git a/comm/mailnews/compose/test/unit/test_nsMsgCompose1.js b/comm/mailnews/compose/test/unit/test_nsMsgCompose1.js new file mode 100644 index 0000000000..700232b46e --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_nsMsgCompose1.js @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/** + * Tests nsMsgCompose expandMailingLists. + */ + +/** + * Helper to check population worked as expected. + * + * @param {string} aTo - Text in the To field. + * @param {string} aCheckTo - The expected To addresses (after possible list population) + */ +function checkPopulate(aTo, aCheckTo) { + var msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + + // Set up some basic fields for compose. + var fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + fields.to = aTo; + + // Set up some params + var params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + + params.composeFields = fields; + + msgCompose.initialize(params); + + msgCompose.expandMailingLists(); + let addresses = fields.getHeader("To"); + let checkEmails = MailServices.headerParser.parseDecodedHeader(aCheckTo); + Assert.equal(addresses.length, checkEmails.length); + for (let i = 0; i < addresses.length; i++) { + Assert.equal(addresses[i].name, checkEmails[i].name); + Assert.equal(addresses[i].email, checkEmails[i].email); + } +} + +function run_test() { + loadABFile("../../../data/abLists1", kPABData.fileName); + loadABFile("../../../data/abLists2", kCABData.fileName); + + // Test - Check we can initialize with fewest specified + // parameters and don't fail/crash like we did in bug 411646. + + var msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + + // Set up some params + var params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + + msgCompose.initialize(params); + + // Test - expandMailingLists basic functionality. + + // Re-initialize + msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + + // Set up some basic fields for compose. + var fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // These aren't in the address book copied above. + fields.from = "test1@foo1.invalid"; + fields.to = "test2@foo1.invalid"; + fields.cc = "test3@foo1.invalid"; + fields.bcc = "test4@foo1.invalid"; + + // Set up some params + params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance( + Ci.nsIMsgComposeParams + ); + + params.composeFields = fields; + + msgCompose.initialize(params); + + msgCompose.expandMailingLists(); + Assert.equal(fields.to, "test2@foo1.invalid"); + Assert.equal(fields.cc, "test3@foo1.invalid"); + Assert.equal(fields.bcc, "test4@foo1.invalid"); + + // Test - expandMailingLists with plain text. + + checkPopulate("test4@foo.invalid", "test4@foo.invalid"); + + // Test - expandMailingLists with html. + + checkPopulate("test5@foo.invalid", "test5@foo.invalid"); + + // Test - expandMailingLists with a list of three items. + + checkPopulate( + "TestList1 <TestList1>", + "test1@foo.invalid,test2@foo.invalid,test3@foo.invalid" + ); + + // Test - expandMailingLists with a list of one item. + + checkPopulate("TestList2 <TestList2>", "test4@foo.invalid"); + + checkPopulate("TestList3 <TestList3>", "test5@foo.invalid"); + + // Test - expandMailingLists with items from multiple address books. + + checkPopulate( + "TestList1 <TestList1>, test3@com.invalid", + "test1@foo.invalid,test2@foo.invalid,test3@foo.invalid,test3@com.invalid" + ); + + checkPopulate( + "TestList2 <TestList2>, ListTest2 <ListTest2>", + "test4@foo.invalid,test4@com.invalid" + ); + + checkPopulate( + "TestList3 <TestList3>, ListTest1 <ListTest1>", + "test5@foo.invalid,test1@com.invalid,test2@com.invalid,test3@com.invalid" + ); + + // test bug 254519 rfc 2047 encoding + checkPopulate( + "=?iso-8859-1?Q?Sure=F6name=2C_Forename_Dr=2E?= <pb@bieringer.invalid>", + '"Sure\u00F6name, Forename Dr." <pb@bieringer.invalid>' + ); +} diff --git a/comm/mailnews/compose/test/unit/test_nsMsgCompose2.js b/comm/mailnews/compose/test/unit/test_nsMsgCompose2.js new file mode 100644 index 0000000000..5f234444b8 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_nsMsgCompose2.js @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsMsgCompose functions relating to send listeners. + */ + +let gMsgCompose = null; +let numSendListenerFunctions = 7; + +let gSLAll = new Array(numSendListenerFunctions + 1); + +function sendListener() {} + +sendListener.prototype = { + mReceived: 0, + mAutoRemoveItem: 0, + + onStartSending(aMsgID, aMsgSize) { + this.mReceived |= 0x01; + if (this.mAutoRemoveItem == 0x01) { + gMsgCompose.removeMsgSendListener(this); + } + }, + onProgress(aMsgID, aProgress, aProgressMax) { + this.mReceived |= 0x02; + if (this.mAutoRemoveItem == 0x02) { + gMsgCompose.removeMsgSendListener(this); + } + }, + onStatus(aMsgID, aMsg) { + this.mReceived |= 0x04; + if (this.mAutoRemoveItem == 0x04) { + gMsgCompose.removeMsgSendListener(this); + } + }, + onStopSending(aMsgID, aStatus, aMsg, aReturnFile) { + this.mReceived |= 0x08; + if (this.mAutoRemoveItem == 0x08) { + gMsgCompose.removeMsgSendListener(this); + } + }, + onGetDraftFolderURI(aMsgID, aFolderURI) { + this.mReceived |= 0x10; + if (this.mAutoRemoveItem == 0x10) { + gMsgCompose.removeMsgSendListener(this); + } + }, + onSendNotPerformed(aMsgID, aStatus) { + this.mReceived |= 0x20; + if (this.mAutoRemoveItem == 0x20) { + gMsgCompose.removeMsgSendListener(this); + } + }, + onTransportSecurityError(msgID, status, secInfo, location) { + this.mReceived |= 0x40; + if (this.mAutoRemoveItem == 0x40) { + gMsgCompose.removeMsgSendListener(this); + } + }, +}; + +function NotifySendListeners() { + gMsgCompose.onStartSending(null, null); + gMsgCompose.onProgress(null, null, null); + gMsgCompose.onStatus(null, null); + gMsgCompose.onStopSending(null, null, null, null); + gMsgCompose.onGetDraftFolderURI(null, null); + gMsgCompose.onSendNotPerformed(null, null); + gMsgCompose.onTransportSecurityError(null, null, null, ""); +} + +function run_test() { + gMsgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + gMsgCompose.initialize(params); + + Assert.ok(gMsgCompose != null); + + // Test - Add a listener + + for (let i = 0; i < numSendListenerFunctions + 1; ++i) { + gSLAll[i] = new sendListener(); + gMsgCompose.addMsgSendListener(gSLAll[i]); + } + + // Test - Notify all listeners + + NotifySendListeners(); + + const bitMask = (1 << numSendListenerFunctions) - 1; + for (let i = 0; i < numSendListenerFunctions + 1; ++i) { + Assert.equal(gSLAll[i].mReceived, bitMask); + gSLAll[i].mReceived = 0; + + // And prepare for test 3. + gSLAll[i].mAutoRemoveItem = 1 << i; + } + + // Test - Remove some listeners as we go + + NotifySendListeners(); + + let currentReceived = 0; + + for (let i = 0; i < numSendListenerFunctions + 1; ++i) { + if (i < numSendListenerFunctions) { + currentReceived += 1 << i; + } + + Assert.equal(gSLAll[i].mReceived, currentReceived); + gSLAll[i].mReceived = 0; + } + + // Test - Ensure the listeners have been removed. + + NotifySendListeners(); + + for (let i = 0; i < numSendListenerFunctions + 1; ++i) { + if (i < numSendListenerFunctions) { + Assert.equal(gSLAll[i].mReceived, 0); + } else { + Assert.equal(gSLAll[i].mReceived, bitMask); + } + } + + // Test - Remove main listener + + gMsgCompose.removeMsgSendListener(gSLAll[numSendListenerFunctions]); +} diff --git a/comm/mailnews/compose/test/unit/test_nsMsgCompose3.js b/comm/mailnews/compose/test/unit/test_nsMsgCompose3.js new file mode 100644 index 0000000000..71521abff4 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_nsMsgCompose3.js @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for increasing the popularity of contacts via + * expandMailingLists. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var TESTS = [ + { + email: "em@test.invalid", + // TB 2 stored popularity as hex, so we need to check correct handling. + prePopularity: "a", + postPopularity: "11", + }, + { + email: "e@test.invalid", + prePopularity: "0", + postPopularity: "1", + }, + { + email: "e@test.invalid", + prePopularity: "1", + postPopularity: "2", + }, + { + email: "em@test.invalid", + prePopularity: "11", + postPopularity: "12", + }, +]; + +function checkPopulate(aTo, aCheckTo) { + let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + + // Set up some basic fields for compose. + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + fields.to = aTo; + + // Set up some params + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + + params.composeFields = fields; + + msgCompose.initialize(params); + + Assert.ok(!msgCompose.expandMailingLists()); + + Assert.equal(fields.to, aCheckTo); +} + +function run_test() { + loadABFile("../../../data/tb2hexpopularity", kPABData.fileName); + + // Check the popularity index on a couple of cards. + let AB = MailServices.ab.getDirectory(kPABData.URI); + + for (let i = 0; i < TESTS.length; ++i) { + let card = AB.cardForEmailAddress(TESTS[i].email); + Assert.ok(!!card); + + // Thunderbird 2 stored its popularityIndexes as hex, hence when we read it + // now we're going to get a hex value. The AB has a value of "a". + Assert.equal( + card.getProperty("PopularityIndex", -1), + TESTS[i].prePopularity + ); + + // Call the check populate function. + checkPopulate(TESTS[i].email, TESTS[i].email); + + // Now we've run check populate, check the popularityIndex has increased. + card = AB.cardForEmailAddress(TESTS[i].email); + Assert.ok(!!card); + + // Thunderbird 2 stored its popularityIndexes as hex, hence when we read it + // now we're going to get a hex value. The AB has a value of "a". + Assert.equal( + card.getProperty("PopularityIndex", -1), + TESTS[i].postPopularity + ); + } +} diff --git a/comm/mailnews/compose/test/unit/test_nsSmtpService1.js b/comm/mailnews/compose/test/unit/test_nsSmtpService1.js new file mode 100644 index 0000000000..d062ef3f1e --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_nsSmtpService1.js @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsSmtpService + */ + +var SmtpServiceContractID = "@mozilla.org/messengercompose/smtp;1"; +var nsISmtpService = Ci.nsISmtpService; + +function run_test() { + var smtpService = Cc[SmtpServiceContractID].getService(nsISmtpService); + + // Test - no servers + + var smtpServers = smtpService.servers; + Assert.equal(smtpServers.length, 0); + + Assert.equal(smtpService.defaultServer, null); + + // Test - add single server, and check + + var smtpServer = smtpService.createServer(); + + smtpServer.hostname = "localhost"; + smtpServer.description = "test"; + + smtpService.defaultServer = smtpServer; + + // Test - Check to see there is only one element in the server list + smtpServers = smtpService.servers; + Assert.ok(smtpServers.length == 1); + + // Test - Find the server in different ways + Assert.equal(smtpServer, smtpService.findServer("", "localhost")); + Assert.equal(smtpServer, smtpService.getServerByKey(smtpServer.key)); + + // Test - Try finding one that doesn't exist. + Assert.equal(null, smtpService.findServer("", "test")); + + // Test - Check default server is still ok + Assert.equal(smtpServer, smtpService.defaultServer); + + // Test - Delete the only server + smtpService.deleteServer(smtpServer); + + smtpServers = smtpService.servers; + Assert.ok(smtpServers.length == 0); + + // do_check_eq(null, smtpService.defaultServer); + + // Test - add multiple servers + + var smtpServerArray = new Array(3); + + for (let i = 0; i < 3; ++i) { + smtpServerArray[i] = smtpService.createServer(); + } + + smtpServerArray[0].hostname = "localhost"; + smtpServerArray[0].description = "test"; + smtpServerArray[0].username = "user"; + + smtpServerArray[1].hostname = "localhost"; + smtpServerArray[1].description = "test1"; + smtpServerArray[1].username = "user1"; + + smtpServerArray[2].hostname = "localhost1"; + smtpServerArray[2].description = "test2"; + smtpServerArray[2].username = ""; + + // Now check them + smtpServers = smtpService.servers; + + var found = [false, false, false]; + + for (smtpServer of smtpServers) { + for (let i = 0; i < 3; ++i) { + if (smtpServer.key == smtpServerArray[i].key) { + found[i] = true; + } + } + } + + Assert.equal(found, "true,true,true"); + + // Test - Find the servers. + + Assert.equal( + smtpServerArray[0].key, + smtpService.findServer("user", "localhost").key + ); + Assert.equal( + smtpServerArray[1].key, + smtpService.findServer("user1", "localhost").key + ); + Assert.equal( + smtpServerArray[2].key, + smtpService.findServer("", "localhost1").key + ); + + Assert.equal(null, smtpService.findServer("user2", "localhost")); + + // XXX: FIXME + // do_check_eq(null, smtpService.findServer("", "localhost")); + + for (let i = 0; i < 3; ++i) { + Assert.equal( + smtpServerArray[i].key, + smtpService.getServerByKey(smtpServerArray[i].key).key + ); + } + + smtpService.defaultServer = smtpServerArray[2]; + Assert.equal( + smtpService.defaultServer.key, + smtpServerArray[2].key, + "Default server should be correctly set" + ); + + // Test - Delete the servers + + for (let i = 0; i < 3; ++i) { + smtpService.deleteServer(smtpServerArray[i]); + } + + smtpServers = smtpService.servers; + Assert.ok(smtpServers.length == 0); +} diff --git a/comm/mailnews/compose/test/unit/test_saveDraft.js b/comm/mailnews/compose/test/unit/test_saveDraft.js new file mode 100644 index 0000000000..b3f7029bab --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_saveDraft.js @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for checking correctly saved as draft with unread. + */ + +add_task(async function checkDraft() { + await createMessage(); + Assert.equal(gDraftFolder.getTotalMessages(false), 1); + Assert.equal(gDraftFolder.getNumUnread(false), 1); +}); + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + run_next_test(); +} diff --git a/comm/mailnews/compose/test/unit/test_sendBackground.js b/comm/mailnews/compose/test/unit/test_sendBackground.js new file mode 100644 index 0000000000..6d0a59f4f9 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendBackground.js @@ -0,0 +1,223 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Tests sending a message in the background (checks auto-send works). + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var server; +var originalData; +var finished = false; +var identity = null; +var testFile1 = do_get_file("data/429891_testcase.eml"); +var testFile2 = do_get_file("data/message1.eml"); + +var kTestFile1Sender = "from_A@foo.invalid"; +var kTestFile1Recipient = "to_A@foo.invalid"; + +var kIdentityMail = "identity@foo.invalid"; + +var gMsgSendLater; + +// This listener handles the post-sending of the actual message and checks the +// sequence and ensures the data is correct. +function msll() {} + +msll.prototype = { + _initialTotal: 0, + + // nsIMsgSendLaterListener + onStartSending(aTotal) { + this._initialTotal = 1; + Assert.equal(gMsgSendLater.sendingMessages, true); + Assert.equal(aTotal, 1); + }, + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) {}, + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) {}, + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + do_throw( + "onMessageSendError should not have been called, status: " + aStatus + ); + }, + onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) { + do_test_finished(); + print("msll onStopSending\n"); + try { + Assert.equal(aStatus, 0); + Assert.equal(aTotalTried, 1); + Assert.equal(aSuccessful, 1); + Assert.equal(this._initialTotal, 1); + Assert.equal(gMsgSendLater.sendingMessages, false); + + do_check_transaction(server.playTransaction(), [ + "EHLO test", + "MAIL FROM:<" + + kTestFile1Sender + + "> BODY=8BITMIME SIZE=" + + originalData.length, + "RCPT TO:<" + kTestFile1Recipient + ">", + "DATA", + ]); + + // Compare data file to what the server received + Assert.equal(originalData, server._daemon.post); + + // check there's still one message left in the folder + Assert.equal( + gMsgSendLater.getUnsentMessagesFolder(null).getTotalMessages(false), + 1 + ); + + finished = true; + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } + }, +}; + +add_task(async function run_the_test() { + // The point of this test - send in background. + Services.prefs.setBoolPref("mailnews.sendInBackground", true); + + // Ensure we have a local mail account, an normal account and appropriate + // servers and identities. + localAccountUtils.loadLocalMailAccount(); + + // Now load (and internally initialize) the send later service + gMsgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater + ); + + // Test file - for bug 429891 + originalData = await IOUtils.readUTF8(testFile1.path); + + // Check that the send later service thinks we don't have messages to send + Assert.equal(gMsgSendLater.hasUnsentMessages(identity), false); + + MailServices.accounts.setSpecialFolders(); + + let account = MailServices.accounts.createAccount(); + let incomingServer = MailServices.accounts.createIncomingServer( + "test", + "localhost", + "pop3" + ); + + // Start the fake SMTP server + server = setupServerDaemon(); + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + identity = getSmtpIdentity(kIdentityMail, smtpServer); + + account.addIdentity(identity); + account.defaultIdentity = identity; + account.incomingServer = incomingServer; + MailServices.accounts.defaultAccount = account; + + localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + Assert.equal(identity.doFcc, true); + + // Now prepare to actually "send" the message later, i.e. dump it in the + // unsent messages folder. + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // Setting the compFields sender and recipient to any value is required to + // survive mime_sanity_check_fields in nsMsgCompUtils.cpp. + // Sender and recipient are required for sendMessageFile but SMTP + // transaction values will be used directly from mail body. + compFields.from = "irrelevant@foo.invalid"; + compFields.to = "irrelevant@foo.invalid"; + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + var msgSend2 = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // A test to check that we are sending files correctly, including checking + // what the server receives and what we output. + test = "sendMessageLater"; + + var messageListener = new msll(); + + gMsgSendLater.addListener(messageListener); + + // Send this message later - it shouldn't get sent + msgSend.sendMessageFile( + identity, + "", + compFields, + testFile2, + false, + false, + Ci.nsIMsgSend.nsMsgQueueForLater, + null, + null, + null, + null + ); + + // Send the unsent message in the background, because we have + // mailnews.sendInBackground set, nsMsgSendLater should just send it for + // us. + msgSend2.sendMessageFile( + identity, + "", + compFields, + testFile1, + false, + false, + Ci.nsIMsgSend.nsMsgDeliverBackground, + null, + null, + null, + null + ); + + server.performTest(); + + do_timeout(10000, function () { + if (!finished) { + do_throw("Notifications of message send/copy not received"); + } + }); + + do_test_pending(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js b/comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js new file mode 100644 index 0000000000..56ab77c303 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendMailAddressIDN.js @@ -0,0 +1,231 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Tests sending messages to addresses with non-ASCII characters. + */ +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +var test = null; +var server; +var finished = false; + +var sentFolder; + +var kSender = "from@foo.invalid"; +var kToASCII = "to@foo.invalid"; +var kToValid = "to@v\u00E4lid.foo.invalid"; +var kToValidACE = "to@xn--vlid-loa.foo.invalid"; +var kToInvalid = "b\u00F8rken.to@invalid.foo.invalid"; +var kToInvalidWithoutDomain = "b\u00F8rken.to"; +var NS_ERROR_ILLEGAL_LOCALPART = 0x80553139; + +// for alertTestUtils.js +let resolveAlert; +function alertPS(parent, aDialogText, aText) { + var composeProps = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + var expectedAlertMessage = + composeProps.GetStringFromName("sendFailed") + + "\n" + + composeProps + .GetStringFromName("errorIllegalLocalPart2") + // Without the domain, we currently don't display any name in the + // message part. + .replace("%s", test == kToInvalidWithoutDomain ? "" : test); + + // we should only get here for the kToInvalid test case + Assert.equal(aText, expectedAlertMessage); + resolveAlert(); +} + +// message listener implementations +function MsgSendListener(aRecipient, originalData) { + this.rcpt = aRecipient; + this.originalData = originalData; +} + +/** + * @implements {nsIMsgSendListener} + * @implements {nsIMsgCopyServiceListener} + */ +MsgSendListener.prototype = { + // nsIMsgSendListener + onStartSending(aMsgID, aMsgSize) {}, + onProgress(aMsgID, aProgress, aProgressMax) {}, + onStatus(aMsgID, aMsg) {}, + onStopSending(aMsgID, aStatus, aMsg, aReturnFile) { + try { + if (test == kToValid || test == kToASCII) { + Assert.equal(aStatus, 0); + do_check_transaction(server.playTransaction(), [ + "EHLO test", + "MAIL FROM:<" + + kSender + + "> BODY=8BITMIME SIZE=" + + this.originalData.length, + "RCPT TO:<" + this.rcpt + ">", + "DATA", + ]); + // Compare data file to what the server received + Assert.equal(this.originalData, server._daemon.post); + } else { + Assert.equal(aStatus, NS_ERROR_ILLEGAL_LOCALPART); + do_check_transaction(server.playTransaction(), ["EHLO test"]); + // Local address (before the @) has non-ascii char(s) or the @ is + // missing from the address. An alert is triggered after the EHLO is + // sent. Nothing else occurs so we "finish" the test to avoid + // NS_ERROR_ABORT test failure due to timeout waiting for the send + // (which doesn't occurs) to complete. + } + } catch (e) { + do_throw(e); + } finally { + server.stop(); + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(false); + } + do_test_finished(); + } + }, + onGetDraftFolderURI(aMsgID, aFolderURI) {}, + onSendNotPerformed(aMsgID, aStatus) {}, + onTransportSecurityError(msgID, status, secInfo, location) {}, + + // nsIMsgCopyServiceListener + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + GetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + Assert.equal(aStatus, 0); + try { + // Now do a comparison of what is in the sent mail folder + let msgData = mailTestUtils.loadMessageToString( + sentFolder, + mailTestUtils.firstMsgHdr(sentFolder) + ); + // Skip the headers etc that mailnews adds + var pos = msgData.indexOf("From:"); + Assert.notEqual(pos, -1); + msgData = msgData.substr(pos); + Assert.equal(this.originalData, msgData); + } catch (e) { + do_throw(e); + } finally { + finished = true; + } + }, + + // QueryInterface + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgSendListener", + "nsIMsgCopyServiceListener", + ]), +}; + +async function doSendTest(aRecipient, aRecipientExpected, waitForPrompt) { + info(`Testing send to ${aRecipient} will get sent to ${aRecipientExpected}`); + let promiseAlertReceived = new Promise(resolve => { + resolveAlert = resolve; + }); + test = aRecipient; + server = setupServerDaemon(); + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kSender, smtpServer); + Assert.equal(identity.doFcc, true); + + // Random test file with data we don't actually care about. ;-) + var testFile = do_get_file("data/message1.eml"); + var originalData = await IOUtils.readUTF8(testFile.path); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + do_test_pending(); + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + compFields.from = identity.email; + compFields.to = aRecipient; + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + msgSend.sendMessageFile( + identity, + "", + compFields, + testFile, + false, + false, + Ci.nsIMsgSend.nsMsgDeliverNow, + null, + new MsgSendListener(aRecipientExpected, originalData), + null, + null + ); + + server.performTest(); + do_timeout(10000, function () { + if (!finished) { + do_throw("Notifications of message send/copy not received"); + } + }); + if (waitForPrompt) { + await promiseAlertReceived; + } + } catch (e) { + Assert.ok(false, "Send fail: " + e); + } finally { + server.stop(); + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +} + +add_setup(function () { + registerAlertTestUtils(); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + MailServices.accounts.setSpecialFolders(); + sentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent"); +}); + +add_task(async function plainASCIIRecipient() { + // Test 1: + // Plain ASCII recipient address. + await doSendTest(kToASCII, kToASCII, false); +}); + +add_task(async function domainContainsNonAscii() { + // Test 2: + // The recipient's domain part contains a non-ASCII character, hence the + // address needs to be converted to ACE before sending. + // The old code would just strip the non-ASCII character and try to send + // the message to the remaining - wrong! - address. + // The new code will translate the domain part to ACE for the SMTP + // transaction (only), i.e. the To: header will stay as stated by the sender. + await doSendTest(kToValid, kToValidACE, false); +}); + +add_task(async function localContainsNonAscii() { + // Test 3: + // The recipient's local part contains a non-ASCII character, which is not + // allowed with unextended SMTP. + // The old code would just strip the invalid character and try to send the + // message to the remaining - wrong! - address. + // The new code will present an informational message box and deny sending. + await doSendTest(kToInvalid, kToInvalid, true); +}); + +add_task(async function invalidCharNoAt() { + // Test 4: + // Bug 856506. invalid char without '@' causes crash. + await doSendTest(kToInvalidWithoutDomain, kToInvalidWithoutDomain, true); +}); diff --git a/comm/mailnews/compose/test/unit/test_sendMailMessage.js b/comm/mailnews/compose/test/unit/test_sendMailMessage.js new file mode 100644 index 0000000000..ea294a0b92 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendMailMessage.js @@ -0,0 +1,189 @@ +/** + * Protocol tests for SMTP. + * + * This test currently consists of verifying the correct protocol sequence + * between mailnews and SMTP server. It does not check the data of the message + * either side of the link, it will be extended later to do that. + */ +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var server; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var kUsername = "testsmtp"; +var kPassword = "smtptest"; + +async function test_RFC2821() { + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Just a basic test to check we're sending mail correctly. + test = "Basic sendMailMessage"; + + // First do test with identity email address used for smtp MAIL FROM. + Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", false); + + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + var transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "MAIL FROM:<" + kIdentityMail + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + + smtpServer.closeCachedConnections(); + server.resetTest(); + + // Now do the same test with sender's email address used for smtp MAIL FROM. + Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", true); + + urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + + smtpServer.closeCachedConnections(); + server.resetTest(); + + // This time with auth. + test = "Auth sendMailMessage"; + + smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext; + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + smtpServer.password = kPassword; + + // First do test with identity email address used for smtp MAIL FROM. + Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", false); + + urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword), + "MAIL FROM:<" + kIdentityMail + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + + smtpServer.closeCachedConnections(); + server.resetTest(); + + // Now do the same test with sender's email address used for smtp MAIL FROM. + Services.prefs.setBoolPref("mail.smtp.useSenderForSmtpMailFrom", true); + + urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword), + "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +} + +add_task(async function run() { + server = setupServerDaemon(); + await test_RFC2821(); +}); diff --git a/comm/mailnews/compose/test/unit/test_sendMessageFile.js b/comm/mailnews/compose/test/unit/test_sendMessageFile.js new file mode 100644 index 0000000000..cb2882e88f --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendMessageFile.js @@ -0,0 +1,172 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Protocol tests for SMTP. + * + * This test verifies: + * - Sending a message to an SMTP server (which is also covered elsewhere). + * - Correct reception of the message by the SMTP server. + * - Correct saving of the message to the sent folder. + * + * Originally written to test bug 429891 where saving to the sent folder was + * mangling the message. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var server; +var sentFolder; +var originalData; +var finished = false; + +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; + +function msl() {} + +msl.prototype = { + // nsIMsgSendListener + onStartSending(aMsgID, aMsgSize) {}, + onProgress(aMsgID, aProgress, aProgressMax) {}, + onStatus(aMsgID, aMsg) {}, + onStopSending(aMsgID, aStatus, aMsg, aReturnFile) { + try { + Assert.equal(aStatus, 0); + + do_check_transaction(server.playTransaction(), [ + "EHLO test", + "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=" + originalData.length, + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + + // Compare data file to what the server received + Assert.equal(originalData, server._daemon.post); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(false); + } + } + }, + onGetDraftFolderURI(aMsgID, aFolderURI) {}, + onSendNotPerformed(aMsgID, aStatus) {}, + onTransportSecurityError(msgID, status, secInfo, location) {}, + + // nsIMsgCopyServiceListener + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + GetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + Assert.equal(aStatus, 0); + try { + // Now do a comparison of what is in the sent mail folder + let msgData = mailTestUtils.loadMessageToString( + sentFolder, + mailTestUtils.firstMsgHdr(sentFolder) + ); + + // Skip the headers etc that mailnews adds + var pos = msgData.indexOf("From:"); + Assert.notEqual(pos, -1); + + msgData = msgData.substr(pos); + + Assert.equal(originalData, msgData); + } catch (e) { + do_throw(e); + } finally { + finished = true; + do_test_finished(); + } + }, + + // QueryInterface + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgSendListener", + "nsIMsgCopyServiceListener", + ]), +}; + +add_task(async function run_the_test() { + server = setupServerDaemon(); + + // Test file - for bug 429891 + var testFile = do_get_file("data/429891_testcase.eml"); + originalData = await IOUtils.readUTF8(testFile.path); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + MailServices.accounts.setSpecialFolders(); + + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kSender, smtpServer); + + sentFolder = localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + Assert.equal(identity.doFcc, true); + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // A test to check that we are sending files correctly, including checking + // what the server receives and what we output. + test = "sendMessageFile"; + + // Msg Comp Fields + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + compFields.from = identity.email; + compFields.to = kTo; + + var messageListener = new msl(); + + msgSend.sendMessageFile( + identity, + "", + compFields, + testFile, + false, + false, + Ci.nsIMsgSend.nsMsgDeliverNow, + null, + messageListener, + null, + null + ); + + server.performTest(); + + do_timeout(10000, function () { + if (!finished) { + do_throw("Notifications of message send/copy not received"); + } + }); + + do_test_pending(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_sendMessageLater.js b/comm/mailnews/compose/test/unit/test_sendMessageLater.js new file mode 100644 index 0000000000..7dcaf8ec32 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendMessageLater.js @@ -0,0 +1,261 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Protocol tests for SMTP. + * + * This test verifies: + * - Sending a message to an SMTP server (which is also covered elsewhere). + * - Correct reception of the message by the SMTP server. + * - Correct saving of the message to the sent folder. + * + * Originally written to test bug 429891 where saving to the sent folder was + * mangling the message. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var server; +var smtpServer; +var originalData; +var finished = false; +var identity = null; +var testFile = do_get_file("data/429891_testcase.eml"); +var kTestFileSender = "from_A@foo.invalid"; +var kTestFileRecipient = "to_A@foo.invalid"; + +var kIdentityMail = "identity@foo.invalid"; + +var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater +); + +// This listener handles the post-sending of the actual message and checks the +// sequence and ensures the data is correct. +function msll() {} + +msll.prototype = { + _initialTotal: 0, + _startedSending: false, + + // nsIMsgSendLaterListener + onStartSending(aTotalMessageCount) { + this._initialTotal = 1; + Assert.equal(msgSendLater.sendingMessages, true); + }, + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) { + this._startedSending = true; + }, + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) { + // XXX Enable this function + }, + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + do_throw( + "onMessageSendError should not have been called, status: " + aStatus + ); + }, + onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) { + do_test_finished(); + print("msll onStopSending\n"); + try { + Assert.equal(this._startedSending, true); + Assert.equal(aStatus, 0); + Assert.equal(aTotalTried, 1); + Assert.equal(aSuccessful, 1); + Assert.equal(this._initialTotal, 1); + Assert.equal(msgSendLater.sendingMessages, false); + + do_check_transaction(server.playTransaction(), [ + "EHLO test", + "MAIL FROM:<" + + kTestFileSender + + "> BODY=8BITMIME SIZE=" + + originalData.length, + "RCPT TO:<" + kTestFileRecipient + ">", + "DATA", + ]); + + // Compare data file to what the server received + Assert.equal(originalData, server._daemon.post); + + finished = true; + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } + }, +}; + +/* exported OnStopCopy */ +// for head_compose.js +function OnStopCopy(aStatus) { + dump("OnStopCopy()\n"); + + try { + Assert.equal(aStatus, 0); + + // Check this is false before we start sending + Assert.equal(msgSendLater.sendingMessages, false); + + let folder = msgSendLater.getUnsentMessagesFolder(identity); + + // Check we have a message in the unsent message folder + Assert.equal(folder.getTotalMessages(false), 1); + + // Check that the send later service thinks we have messages to send + Assert.equal(msgSendLater.hasUnsentMessages(identity), true); + + // Now do a comparison of what is in the sent mail folder + let msgData = mailTestUtils.loadMessageToString( + folder, + mailTestUtils.firstMsgHdr(folder) + ); + // Skip the headers etc that mailnews adds + var pos = msgData.indexOf("From:"); + Assert.notEqual(pos, -1); + + msgData = msgData.substr(pos); + + // Check the data is matching. + Assert.equal(originalData, msgData); + + sendMessageLater(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + finished = true; + } +} + +// This function does the actual send later +function sendMessageLater() { + // Set up the SMTP server. + server = setupServerDaemon(); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server + server.start(); + smtpServer.port = server.port; + + // A test to check that we are sending files correctly, including checking + // what the server receives and what we output. + test = "sendMessageLater"; + + var messageListener = new msll(); + + msgSendLater.addListener(messageListener); + + // Send the unsent message + msgSendLater.sendUnsentMessages(identity); + + server.performTest(); + + do_timeout(10000, function () { + if (!finished) { + do_throw("Notifications of message send/copy not received"); + } + }); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +} + +add_task(async function run_the_test() { + // Test file - for bug 429891 + originalData = await IOUtils.readUTF8(testFile.path); + + // Ensure we have a local mail account, an normal account and appropriate + // servers and identities. + localAccountUtils.loadLocalMailAccount(); + + // Check that the send later service thinks we don't have messages to send + Assert.equal(msgSendLater.hasUnsentMessages(identity), false); + + MailServices.accounts.setSpecialFolders(); + + let account = MailServices.accounts.createAccount(); + let incomingServer = MailServices.accounts.createIncomingServer( + "test", + "localhost", + "pop3" + ); + + smtpServer = getBasicSmtpServer(1); + identity = getSmtpIdentity(kIdentityMail, smtpServer); + + account.addIdentity(identity); + account.defaultIdentity = identity; + account.incomingServer = incomingServer; + MailServices.accounts.defaultAccount = account; + + localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + Assert.equal(identity.doFcc, true); + + // Now prepare to actually "send" the message later, i.e. dump it in the + // unsent messages folder. + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // Setting the compFields sender and recipient to any value is required to + // survive mime_sanity_check_fields in nsMsgCompUtils.cpp. + // Sender and recipient are required for sendMessageFile but SMTP + // transaction values will be used directly from mail body. + compFields.from = "irrelevant@foo.invalid"; + compFields.to = "irrelevant@foo.invalid"; + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + msgSend.sendMessageFile( + identity, + "", + compFields, + testFile, + false, + false, + Ci.nsIMsgSend.nsMsgQueueForLater, + null, + copyListener, + null, + null + ); + + // Now we wait till we get copy notification of completion. + do_test_pending(); +}); diff --git a/comm/mailnews/compose/test/unit/test_sendMessageLater2.js b/comm/mailnews/compose/test/unit/test_sendMessageLater2.js new file mode 100644 index 0000000000..bd0b974400 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendMessageLater2.js @@ -0,0 +1,301 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/** + * Complex test for the send message later function - including sending multiple + * times in the same session. + * + * XXX: This test is intended to additionally test sending of multiple messages + * from one send later instance, however due to the fact we use one connection + * per message sent, it is very difficult to consistently get the fake server + * reconnected in time for the next connection. Thus, sending of multiple + * messages is currently disabled (but commented out for local testing if + * required), when we fix bug 136871 we should be able to enable the multiple + * messages option. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +var server = null; +var smtpServer; +var gSentFolder; +var identity = null; +var gMsgFile = [ + do_get_file("data/message1.eml"), + do_get_file("data/429891_testcase.eml"), +]; +var kTestFileSender = ["from_B@foo.invalid", "from_A@foo.invalid"]; +var kTestFileRecipient = ["to_B@foo.invalid", "to_A@foo.invalid"]; + +var gMsgFileData = []; +var gMsgOrder = []; +var gLastSentMessage = 0; + +var kIdentityMail = "identity@foo.invalid"; + +var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater +); + +var messageListener; +var onStopCopyPromise = PromiseUtils.defer(); + +/* exported OnStopCopy */ +// for head_compose.js +// This function is used to find out when the copying of the message to the +// unsent message folder is completed, and hence can fire off the actual +// sending of the message. +function OnStopCopy(aStatus) { + Assert.equal(aStatus, 0); + + // Check this is false before we start sending. + Assert.equal(msgSendLater.sendingMessages, false); + + // Check that the send later service thinks we have messages to send. + Assert.equal(msgSendLater.hasUnsentMessages(identity), true); + + // Check we have a message in the unsent message folder. + Assert.equal(gSentFolder.getTotalMessages(false), gMsgOrder.length); + + // Start the next step after a brief time so that functions can finish + // properly. + onStopCopyPromise.resolve(); +} + +add_setup(async function () { + // Load in the test files so we have a record of length and their data. + for (var i = 0; i < gMsgFile.length; ++i) { + gMsgFileData[i] = await IOUtils.readUTF8(gMsgFile[i].path); + } + + // Ensure we have a local mail account, an normal account and appropriate + // servers and identities. + localAccountUtils.loadLocalMailAccount(); + + // Check that the send later service thinks we don't have messages to send. + Assert.equal(msgSendLater.hasUnsentMessages(identity), false); + + MailServices.accounts.setSpecialFolders(); + + let account = MailServices.accounts.createAccount(); + let incomingServer = MailServices.accounts.createIncomingServer( + "test", + "localhost", + "pop3" + ); + + smtpServer = getBasicSmtpServer(1); + identity = getSmtpIdentity(kIdentityMail, smtpServer); + + account.addIdentity(identity); + account.defaultIdentity = identity; + account.incomingServer = incomingServer; + MailServices.accounts.defaultAccount = account; + + localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + gSentFolder = msgSendLater.getUnsentMessagesFolder(identity); + + // Don't copy messages to sent folder for this test. + identity.doFcc = false; + + // Create and add a listener. + messageListener = new MsgSendLaterListener(); + + msgSendLater.addListener(messageListener); + + // Set up the server. + server = setupServerDaemon(); + server.setDebugLevel(fsDebugRecv); +}); + +add_task(async function test_sendMessageLater2_message1() { + // Copy Message from file to folder. + await sendMessageLater(0); + + // Send unsent message. + await sendUnsentMessages(); + + // Check sent folder is now empty. + Assert.equal(gSentFolder.getTotalMessages(false), 0); + + // Reset the server. + server.stop(); + server.resetTest(); + + // Reset counts. + resetCounts(); +}); + +add_task(async function test_sendMessageLater2_429891_testcase() { + // Copy more messages. + await sendMessageLater(1); + + // XXX Only do one the second time round, as described at the start of the + // file. + // await sendMessageLater(0); + + // Test send again. + await sendUnsentMessages(); +}); + +async function sendMessageLater(aTestFileIndex) { + gMsgOrder.push(aTestFileIndex); + + // Prepare to actually "send" the message later, i.e. dump it in the + // unsent messages folder. + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // Setting the compFields sender and recipient to any value is required to + // survive mime_sanity_check_fields in nsMsgCompUtils.cpp. + // Sender and recipient are required for sendMessageFile but SMTP + // transaction values will be used directly from mail body. + compFields.from = "irrelevant@foo.invalid"; + compFields.to = "irrelevant@foo.invalid"; + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + msgSend.sendMessageFile( + identity, + "", + compFields, + gMsgFile[aTestFileIndex], + false, + false, + Ci.nsIMsgSend.nsMsgQueueForLater, + null, + copyListener, + null, + null + ); + await onStopCopyPromise.promise; + // Reset onStopCopyPromise. + onStopCopyPromise = PromiseUtils.defer(); +} + +function resetCounts() { + gMsgOrder = []; + gLastSentMessage = 0; +} + +// This function does the actual send later. +async function sendUnsentMessages() { + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server. + server.start(); + smtpServer.port = server.port; + + // Send the unsent message. + msgSendLater.sendUnsentMessages(identity); + } catch (e) { + throw new Error(e); + } + await messageListener.promise; + messageListener.deferPromise(); +} + +// This listener handles the post-sending of the actual message and checks the +// sequence and ensures the data is correct. +class MsgSendLaterListener { + constructor() { + this._deferredPromise = PromiseUtils.defer(); + } + + checkMessageSend(aCurrentMessage) { + do_check_transaction(server.playTransaction(), [ + "EHLO test", + "MAIL FROM:<" + + kTestFileSender[gMsgOrder[aCurrentMessage - 1]] + + "> BODY=8BITMIME SIZE=" + + gMsgFileData[gMsgOrder[aCurrentMessage - 1]].length, + "RCPT TO:<" + kTestFileRecipient[gMsgOrder[aCurrentMessage - 1]] + ">", + "DATA", + ]); + + // Compare data file to what the server received. + Assert.equal( + gMsgFileData[gMsgOrder[aCurrentMessage - 1]], + server._daemon.post + ); + } + + // nsIMsgSendLaterListener + onStartSending(aTotalMessageCount) { + Assert.equal(aTotalMessageCount, gMsgOrder.length); + Assert.equal(msgSendLater.sendingMessages, true); + } + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) { + if (gLastSentMessage > 0) { + this.checkMessageSend(aCurrentMessage); + } + Assert.equal(gLastSentMessage + 1, aCurrentMessage); + gLastSentMessage = aCurrentMessage; + } + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) { + Assert.equal(aTotalMessageCount, gMsgOrder.length); + Assert.equal(gLastSentMessage, aCurrentMessage); + Assert.equal(msgSendLater.sendingMessages, true); + } + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + throw new Error( + "onMessageSendError should not have been called, status: " + aStatus + ); + } + onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) { + try { + Assert.equal(aStatus, 0); + Assert.equal(aTotalTried, aSuccessful); + Assert.equal(msgSendLater.sendingMessages, false); + + // Check that the send later service now thinks we don't have messages to + // send. + Assert.equal(msgSendLater.hasUnsentMessages(identity), false); + + this.checkMessageSend(gLastSentMessage); + } catch (e) { + throw new Error(e); + } + // The extra timeout here is to work around an issue where sometimes + // the sendUnsentMessages is completely synchronous up until onStopSending + // and sometimes it isn't. This protects us for the synchronous case to + // allow the sendUnsentMessages function to complete and exit before we + // resolve the promise. + PromiseTestUtils.promiseDelay(0).then(resolve => { + this._deferredPromise.resolve(true); + }); + } + + deferPromise() { + this._deferredPromise = PromiseUtils.defer(); + } + + get promise() { + return this._deferredPromise.promise; + } +} diff --git a/comm/mailnews/compose/test/unit/test_sendMessageLater3.js b/comm/mailnews/compose/test/unit/test_sendMessageLater3.js new file mode 100644 index 0000000000..08e32481c6 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendMessageLater3.js @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Protocol tests for SMTP. + * + * For trying to send a message later with no server connected, this test + * verifies: + * - A correct status response. + * - A correct state at the end of attempting to send. + */ + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var originalData; +var identity = null; +var testFile = do_get_file("data/429891_testcase.eml"); + +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; + +var msgSendLater = Cc["@mozilla.org/messengercompose/sendlater;1"].getService( + Ci.nsIMsgSendLater +); + +// for alertTestUtils.js +function alertPS(parent, aDialogTitle, aText) { + dump("Hiding Alert {\n" + aText + "\n} End Alert\n"); +} + +// This listener handles the post-sending of the actual message and checks the +// sequence and ensures the data is correct. +function msll() {} + +msll.prototype = { + _initialTotal: 0, + _errorRaised: false, + + // nsIMsgSendLaterListener + onStartSending(aTotal) { + this._initialTotal = 1; + Assert.equal(msgSendLater.sendingMessages, true); + }, + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) {}, + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) {}, + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + this._errorRaised = true; + }, + onStopSending(aStatus, aMsg, aTotal, aSuccessful) { + print("msll onStopSending\n"); + + // NS_ERROR_SMTP_SEND_FAILED_REFUSED is 2153066798 + Assert.equal(aStatus, 2153066798); + Assert.equal(aTotal, 1); + Assert.equal(aSuccessful, 0); + Assert.equal(this._initialTotal, 1); + Assert.equal(this._errorRaised, true); + Assert.equal(msgSendLater.sendingMessages, false); + // Check that the send later service still thinks we have messages to send. + Assert.equal(msgSendLater.hasUnsentMessages(identity), true); + + do_test_finished(); + }, +}; + +/* exported OnStopCopy */ +// for head_compose.js +function OnStopCopy(aStatus) { + Assert.equal(aStatus, 0); + + // Check this is false before we start sending + Assert.equal(msgSendLater.sendingMessages, false); + + let folder = msgSendLater.getUnsentMessagesFolder(identity); + + // Check that the send later service thinks we have messages to send. + Assert.equal(msgSendLater.hasUnsentMessages(identity), true); + + // Check we have a message in the unsent message folder + Assert.equal(folder.getTotalMessages(false), 1); + + // Now do a comparison of what is in the unsent mail folder + let msgData = mailTestUtils.loadMessageToString( + folder, + mailTestUtils.firstMsgHdr(folder) + ); + + // Skip the headers etc that mailnews adds + var pos = msgData.indexOf("From:"); + Assert.notEqual(pos, -1); + + msgData = msgData.substr(pos); + + // Check the data is matching. + Assert.equal(originalData, msgData); + + do_timeout(0, sendMessageLater); +} + +// This function does the actual send later +function sendMessageLater() { + // No server for this test, just attempt to send unsent and wait. + var messageListener = new msll(); + + msgSendLater.addListener(messageListener); + + // Send the unsent message + msgSendLater.sendUnsentMessages(identity); +} + +add_task(async function run_the_test() { + registerAlertTestUtils(); + + // Test file - for bug 429891 + originalData = await IOUtils.readUTF8(testFile.path); + + // Ensure we have a local mail account, an normal account and appropriate + // servers and identities. + localAccountUtils.loadLocalMailAccount(); + + // Check that the send later service thinks we don't have messages to send. + Assert.equal(msgSendLater.hasUnsentMessages(identity), false); + + MailServices.accounts.setSpecialFolders(); + + let account = MailServices.accounts.createAccount(); + let incomingServer = MailServices.accounts.createIncomingServer( + "test", + "localhost", + "pop3" + ); + + var smtpServer = getBasicSmtpServer(); + identity = getSmtpIdentity(kSender, smtpServer); + + account.addIdentity(identity); + account.defaultIdentity = identity; + account.incomingServer = incomingServer; + MailServices.accounts.defaultAccount = account; + + localAccountUtils.rootFolder.createLocalSubfolder("Sent"); + + identity.doFcc = false; + + // Now prepare to actually "send" the message later, i.e. dump it in the + // unsent messages folder. + + var compFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + compFields.from = identity.email; + compFields.to = kTo; + + var msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance( + Ci.nsIMsgSend + ); + + msgSend.sendMessageFile( + identity, + "", + compFields, + testFile, + false, + false, + Ci.nsIMsgSend.nsMsgQueueForLater, + null, + copyListener, + null, + null + ); + + // Now we wait till we get copy notification of completion. + do_test_pending(); +}); diff --git a/comm/mailnews/compose/test/unit/test_sendObserver.js b/comm/mailnews/compose/test/unit/test_sendObserver.js new file mode 100644 index 0000000000..3640d1ca02 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_sendObserver.js @@ -0,0 +1,52 @@ +/* + * Tests that the mail-set-sender observer, used by extensions to modify the + * outgoing server, works. + * + * This is adapted from test_messageHeaders.js + */ + +var CompFields = CC( + "@mozilla.org/messengercompose/composefields;1", + Ci.nsIMsgCompFields +); + +// nsIObserver implementation. +var gData = ""; +var observer = { + observe(aSubject, aTopic, aData) { + if (aTopic == "mail-set-sender") { + Assert.ok(aSubject instanceof Ci.nsIMsgCompose); + gData = aData; + } + }, +}; + +add_task(async function testObserver() { + let fields = new CompFields(); + let identity = getSmtpIdentity( + "from@tinderbox.invalid", + getBasicSmtpServer() + ); + identity.fullName = "Observer Tester"; + fields.to = "Emile <nobody@tinderbox.invalid>"; + fields.cc = "Alex <alex@tinderbox.invalid>"; + fields.subject = "Let's test the observer"; + + await richCreateMessage(fields, [], identity); + // observer data should have: + // (no account), Ci.nsIMsgSend.nsMsgSaveAsDraft, identity.key + Assert.equal(gData, ",4,id1"); + + // Now try with an account + await richCreateMessage(fields, [], identity, localAccountUtils.msgAccount); + // observer data should have: + // (local account key), Ci.nsIMsgSend.nsMsgSaveAsDraft, identity.key + Assert.equal(gData, "account1,4,id1"); +}); + +function run_test() { + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + Services.obs.addObserver(observer, "mail-set-sender"); + run_next_test(); +} diff --git a/comm/mailnews/compose/test/unit/test_smtp8bitMime.js b/comm/mailnews/compose/test/unit/test_smtp8bitMime.js new file mode 100644 index 0000000000..d763947154 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtp8bitMime.js @@ -0,0 +1,105 @@ +/** + * 8BITMIME tests for SMTP. + * + * This test verifies that 8BITMIME is sent to the server only if the server + * advertises it AND if mail.strictly_mime doesn't force us to send 7bit. + * It does not check the data of the message on either side of the link. + */ +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var server; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; + +// aStrictMime: Test if mail.strictly_mime omits the BODY=8BITMIME attribute. +// aServer8bit: Test if BODY=8BITMIME is only sent if advertised by the server. + +async function test_8bitmime(aStrictMime, aServer8bit) { + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + test = + "Strictly MIME" + + (aStrictMime ? "on (7bit" : "off (8bit") + + ", 8BITMIME " + + (aServer8bit ? "" : "not ") + + "advertised)"; + + Services.prefs.setBoolPref("mail.strictly_mime", aStrictMime); + + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + var transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "MAIL FROM:<" + + kSender + + (!aStrictMime && aServer8bit + ? "> BODY=8BITMIME SIZE=159" + : "> SIZE=159"), + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + + server.resetTest(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +} + +add_task(async function run() { + // The default SMTP server advertises 8BITMIME capability. + server = setupServerDaemon(); + await test_8bitmime(true, true); + await test_8bitmime(false, true); + + // Now we need a server which does not advertise 8BITMIME capability. + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + handler.kCapabilities = ["SIZE"]; + return handler; + } + server = setupServerDaemon(createHandler); + await test_8bitmime(true, false); + await test_8bitmime(false, false); +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpAuthMethods.js b/comm/mailnews/compose/test/unit/test_smtpAuthMethods.js new file mode 100644 index 0000000000..24d5c6d554 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpAuthMethods.js @@ -0,0 +1,166 @@ +/** + * Authentication tests for SMTP. + * + * Test code <copied from="test_pop3AuthMethods.js"> + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var server; +var kAuthSchemes; +var smtpServer; +var testFile; +var identity; + +var kUsername = "fred"; +var kPassword = "wilma"; +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var MAILFROM = "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159"; +var RCPTTO = "RCPT TO:<" + kTo + ">"; +var AUTHPLAIN = "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword); + +var tests = [ + { + title: + "Cleartext password, with server supporting AUTH PLAIN, LOGIN, and CRAM", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], + expectSuccess: true, + transaction: ["EHLO test", AUTHPLAIN, MAILFROM, RCPTTO, "DATA"], + }, + { + title: "Cleartext password, with server only supporting AUTH LOGIN", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: ["LOGIN"], + expectSuccess: true, + transaction: ["EHLO test", "AUTH LOGIN", MAILFROM, RCPTTO, "DATA"], + }, + { + title: + "Encrypted password, with server supporting AUTH PLAIN, LOGIN and CRAM", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], + expectSuccess: true, + transaction: ["EHLO test", "AUTH CRAM-MD5", MAILFROM, RCPTTO, "DATA"], + }, + { + title: + "Encrypted password, with server only supporting AUTH PLAIN (must fail)", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + serverAuthMethods: ["PLAIN"], + expectSuccess: false, + transaction: ["EHLO test"], + }, + { + title: + "Any secure method, with server supporting AUTH PLAIN, LOGIN and CRAM", + clientAuthMethod: Ci.nsMsgAuthMethod.secure, + serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], + expectSuccess: true, + transaction: ["EHLO test", "AUTH CRAM-MD5", MAILFROM, RCPTTO, "DATA"], + }, + { + title: + "Any secure method, with server only supporting AUTH PLAIN (must fail)", + clientAuthMethod: Ci.nsMsgAuthMethod.secure, + serverAuthMethods: ["PLAIN"], + expectSuccess: false, + transaction: ["EHLO test"], + }, +]; + +function nextTest() { + if (tests.length == 0) { + // this is sync, so we run into endTest() at the end of run_test() now + return; + } + server.resetTest(); + + var curTest = tests.shift(); + test = curTest.title; + dump("NEXT test: " + curTest.title + "\n"); + + // Adapt to curTest + kAuthSchemes = curTest.serverAuthMethods; + smtpServer.authMethod = curTest.clientAuthMethod; + + // Run test + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + let resolved = false; + urlListener.promise.catch(e => {}).finally(() => (resolved = true)); + Services.tm.spinEventLoopUntil("wait for sending", () => resolved); + + do_check_transaction(server.playTransaction(), curTest.transaction); + + smtpServer.closeCachedConnections(); + nextTest(); +} + +function run_test() { + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + handler.kUsername = kUsername; + handler.kPassword = kPassword; + handler.kAuthRequired = true; + handler.kAuthSchemes = kAuthSchemes; + return handler; + } + server = setupServerDaemon(createHandler); + dump("AUTH PLAIN = " + AUTHPLAIN + "\n"); + server.start(); + + localAccountUtils.loadLocalMailAccount(); + smtpServer = getBasicSmtpServer(server.port); + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + smtpServer.password = kPassword; + identity = getSmtpIdentity(kIdentityMail, smtpServer); + + testFile = do_get_file("data/message1.eml"); + + nextTest(); + } catch (e) { + do_throw(e); + } finally { + endTest(); + } +} + +function endTest() { + dump("endTest()\n"); + server.stop(); + + dump("emptying event loop\n"); + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + dump("next event\n"); + thread.processNextEvent(true); + } +} diff --git a/comm/mailnews/compose/test/unit/test_smtpClient.js b/comm/mailnews/compose/test/unit/test_smtpClient.js new file mode 100644 index 0000000000..b06ec48560 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpClient.js @@ -0,0 +1,136 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +let server = setupServerDaemon(); +server.start(); +registerCleanupFunction(() => { + server.stop(); +}); + +/** + * Test sending is aborted when alwaysSTARTTLS is set, but the server doesn't + * support STARTTLS. + */ +add_task(async function testAbort() { + server.resetTest(); + let smtpServer = getBasicSmtpServer(server.port); + let identity = getSmtpIdentity("identity@foo.invalid", smtpServer); + // Set to always use STARTTLS. + smtpServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + + do_test_pending(); + + let urlListener = { + OnStartRunningUrl(url) {}, + OnStopRunningUrl(url, status) { + // Test sending is aborted with NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS. + Assert.equal(status, 0x80553126); + do_test_finished(); + }, + }; + + // Send a message. + let testFile = do_get_file("data/message1.eml"); + MailServices.smtp.sendMailMessage( + testFile, + "to@foo.invalid", + identity, + "from@foo.invalid", + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + server.performTest(); +}); + +/** + * Test client identity extension works. + */ +add_task(async function testClientIdentityExtension() { + server.resetTest(); + let smtpServer = getBasicSmtpServer(server.port); + let identity = getSmtpIdentity("identity@foo.invalid", smtpServer); + // Enable and set clientid to the smtp server. + smtpServer.clientidEnabled = true; + smtpServer.clientid = "uuid-111"; + + // Send a message. + let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener(); + let testFile = do_get_file("data/message1.eml"); + MailServices.smtp.sendMailMessage( + testFile, + "to@foo.invalid", + identity, + "from@foo.invalid", + null, + asyncUrlListener, + null, + null, + false, + "", + {}, + {} + ); + + await asyncUrlListener.promise; + + // Check CLIENTID command is sent. + let transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "CLIENTID UUID uuid-111", + "MAIL FROM:<from@foo.invalid> BODY=8BITMIME SIZE=159", + "RCPT TO:<to@foo.invalid>", + "DATA", + ]); +}); + +/** + * Test that when To and Cc/Bcc contain the same address, should send only + * one RCPT TO per address. + */ +add_task(async function testDeduplicateRecipients() { + server.resetTest(); + let smtpServer = getBasicSmtpServer(server.port); + let identity = getSmtpIdentity("identity@foo.invalid", smtpServer); + + // Send a message, notice to1 appears twice in the recipients argument. + let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener(); + let testFile = do_get_file("data/message1.eml"); + MailServices.smtp.sendMailMessage( + testFile, + "to1@foo.invalid,to2@foo.invalid,to1@foo.invalid", + identity, + "from@foo.invalid", + null, + asyncUrlListener, + null, + null, + false, + "", + {}, + {} + ); + + await asyncUrlListener.promise; + + // Check only one RCPT TO is sent for to1. + let transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "MAIL FROM:<from@foo.invalid> BODY=8BITMIME SIZE=159", + "RCPT TO:<to1@foo.invalid>", + "RCPT TO:<to2@foo.invalid>", + "DATA", + ]); +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpPassword.js b/comm/mailnews/compose/test/unit/test_smtpPassword.js new file mode 100644 index 0000000000..f4b8515df7 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpPassword.js @@ -0,0 +1,97 @@ +/** + * Authentication tests for SMTP. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/passwordStorage.js"); + +var server; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var kUsername = "testsmtp"; +// Password needs to match the login information stored in the signons json +// file. +var kPassword = "smtptest"; + +add_task(async function () { + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + // Username needs to match the login information stored in the signons json + // file. + handler.kUsername = kUsername; + handler.kPassword = kPassword; + handler.kAuthRequired = true; + return handler; + } + server = setupServerDaemon(createHandler); + + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-mailnews1.8.json"); + + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // This time with auth + test = "Auth sendMailMessage"; + + smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext; + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + var transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kPassword), + "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpPassword2.js b/comm/mailnews/compose/test/unit/test_smtpPassword2.js new file mode 100644 index 0000000000..a0445ad0a3 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpPassword2.js @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Extra tests for SMTP passwords (forgetPassword) + */ + +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/passwordStorage.js"); + +var kUser1 = "testsmtp"; +var kUser2 = "testsmtpa"; +var kProtocol = "smtp"; +var kHostname = "localhost"; +var kServerUrl = kProtocol + "://" + kHostname; + +add_task(async function () { + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-mailnews1.8-multiple.json"); + + // Set up the basic accounts and folders. + localAccountUtils.loadLocalMailAccount(); + + var smtpServer1 = getBasicSmtpServer(); + var smtpServer2 = getBasicSmtpServer(); + + smtpServer1.authMethod = 3; + smtpServer1.username = kUser1; + smtpServer2.authMethod = 3; + smtpServer2.username = kUser2; + + // Test - Check there are two logins to begin with. + let logins = Services.logins.findLogins(kServerUrl, null, kServerUrl); + + Assert.equal(logins.length, 2); + + // These will either be one way around or the other. + if (logins[0].username == kUser1) { + Assert.equal(logins[1].username, kUser2); + } else { + Assert.equal(logins[0].username, kUser2); + Assert.equal(logins[1].username, kUser1); + } + + // Test - Remove a login via the incoming server + smtpServer1.forgetPassword(); + + logins = Services.logins.findLogins(kServerUrl, null, kServerUrl); + + // should be one login left for kUser2 + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, kUser2); + + // Test - Remove the other login via the incoming server + smtpServer2.forgetPassword(); + + logins = Services.logins.findLogins(kServerUrl, null, kServerUrl); + + // There should be no login left. + Assert.equal(logins.length, 0); +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js new file mode 100644 index 0000000000..b7d7f1ef43 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure1.js @@ -0,0 +1,151 @@ +/** + * This test checks to see if the smtp password failure is handled correctly. + * The steps are: + * - Have an invalid password in the password database. + * - Check we get a prompt asking what to do. + * - Check retry does what it should do. + * - Check cancel does what it should do. + * + * XXX Due to problems with the fakeserver + smtp not using one connection for + * multiple sends, the rest of this test is in test_smtpPasswordFailure2.js. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/alertTestUtils.js"); +load("../../../resources/passwordStorage.js"); + +var server; +var attempt = 0; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var kUsername = "testsmtp"; +// Login information needs to match the login information stored in the signons +// json file. +var kInvalidPassword = "smtptest"; +var kValidPassword = "smtptest1"; + +/* exported alert, confirmEx */ +// for alertTestUtils.js +function alert(aDialogText, aText) { + // The first few attempts may prompt about the password problem, the last + // attempt shouldn't. + Assert.ok(attempt < 4); + + // Log the fact we've got an alert, but we don't need to test anything here. + dump("Alert Title: " + aDialogText + "\nAlert Text: " + aText + "\n"); +} + +function confirmExPS( + parent, + aDialogTitle, + aText, + aButtonFlags, + aButton0Title, + aButton1Title, + aButton2Title, + aCheckMsg, + aCheckState +) { + switch (++attempt) { + // First attempt, retry. + case 1: + dump("\nAttempting retry\n"); + return 0; + // Second attempt, cancel. + case 2: + dump("\nCancelling login attempt\n"); + return 1; + default: + do_throw("unexpected attempt number " + attempt); + return 1; + } +} + +add_task(async function () { + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + // Username needs to match the login information stored in the signons json + // file. + handler.kUsername = kUsername; + handler.kPassword = kValidPassword; + handler.kAuthRequired = true; + return handler; + } + server = setupServerDaemon(createHandler); + + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-mailnews1.8.json"); + + registerAlertTestUtils(); + + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // This time with auth + test = "Auth sendMailMessage"; + + smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext; + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + + dump("Send\n"); + + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + null, + null, + null, + false, + "", + {}, + {} + ); + + server.performTest(); + + dump("End Send\n"); + + Assert.equal(attempt, 2); + + // Check that we haven't forgetton the login even though we've retried and cancelled. + let logins = Services.logins.findLogins( + "smtp://localhost", + null, + "smtp://localhost" + ); + + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, kUsername); + Assert.equal(logins[0].password, kInvalidPassword); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js new file mode 100644 index 0000000000..f394db434d --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure2.js @@ -0,0 +1,178 @@ +/** + * This test checks to see if the pop3 password failure is handled correctly. + * The steps are: + * - Have an invalid password in the password database. + * - Re-initiate connection, this time select enter new password, check that + * we get a new password prompt and can enter the password. + * + * XXX Due to problems with the fakeserver + smtp not using one connection for + * multiple sends, the first part of this test is in + * test_smtpPasswordFailure2.js. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/alertTestUtils.js"); +load("../../../resources/passwordStorage.js"); + +var server; +var attempt = 0; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var kUsername = "testsmtp"; +// Password needs to match the login information stored in the signons json +// file. +var kInvalidPassword = "smtptest"; +var kValidPassword = "smtptest1"; + +function confirmExPS( + aDialogTitle, + aText, + aButtonFlags, + aButton0Title, + aButton1Title, + aButton2Title, + aCheckMsg, + aCheckState +) { + switch (++attempt) { + // First attempt, retry. + case 1: + dump("\nAttempting Retry\n"); + return 0; + // Second attempt, enter a new password. + case 2: + dump("\nEnter new password\n"); + return 2; + default: + do_throw("unexpected attempt number " + attempt); + return 1; + } +} + +function promptPasswordPS( + aParent, + aDialogTitle, + aText, + aPassword, + aCheckMsg, + aCheckState +) { + if (attempt == 2) { + aPassword.value = kValidPassword; + aCheckState.value = true; + return true; + } + return false; +} + +add_task(async function () { + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + // Username needs to match the login information stored in the signons json + // file. + handler.kUsername = kUsername; + handler.kPassword = kValidPassword; + handler.kAuthRequired = true; + handler.kAuthSchemes = ["PLAIN", "LOGIN"]; // make match expected transaction below + return handler; + } + server = setupServerDaemon(createHandler); + + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-mailnews1.8.json"); + + registerAlertTestUtils(); + + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + // Handle the server in a try/catch/finally loop so that we always will stop + // the server if something fails. + try { + // Start the fake SMTP server + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // This time with auth + test = "Auth sendMailMessage"; + + smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext; + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + + dump("Send\n"); + + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + + await urlListener.promise; + + dump("End Send\n"); + + Assert.equal(attempt, 2); + + var transaction = server.playTransaction(); + do_check_transaction(transaction, [ + "EHLO test", + // attempt 3 invalid password + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kInvalidPassword), + "AUTH LOGIN", + // attempt 4 which retries + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kInvalidPassword), + "AUTH LOGIN", + // then we enter the correct password + "AUTH PLAIN " + AuthPLAIN.encodeLine(kUsername, kValidPassword), + "MAIL FROM:<" + kSender + "> BODY=8BITMIME SIZE=159", + "RCPT TO:<" + kTo + ">", + "DATA", + ]); + + // Now check the new one has been saved. + let logins = Services.logins.findLogins( + "smtp://localhost", + null, + "smtp://localhost" + ); + + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, kUsername); + Assert.equal(logins[0].password, kValidPassword); + do_test_finished(); + } catch (e) { + do_throw(e); + } finally { + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + } +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js new file mode 100644 index 0000000000..27312b47a4 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpPasswordFailure3.js @@ -0,0 +1,154 @@ +/** + * This test checks to see if the smtp password failure is handled correctly + * when the server drops the connection on an authentication error. + * The steps are: + * - Have an invalid password in the password database. + * - Re-initiate connection, this time select enter new password, check that + * we get a new password prompt and can enter the password. + * + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/alertTestUtils.js"); +load("../../../resources/passwordStorage.js"); + +var server; +var attempt = 0; + +var kIdentityMail = "identity@foo.invalid"; +var kSender = "from@foo.invalid"; +var kTo = "to@foo.invalid"; +var kUsername = "testsmtp"; +// Password needs to match the login information stored in the signons json +// file. +var kValidPassword = "smtptest1"; + +function confirmExPS( + aDialogTitle, + aText, + aButtonFlags, + aButton0Title, + aButton1Title, + aButton2Title, + aCheckMsg, + aCheckState +) { + switch (++attempt) { + // First attempt, retry. + case 1: + dump("\nAttempting Retry\n"); + return 0; + // Second attempt, enter a new password. + case 2: + dump("\nEnter new password\n"); + return 2; + default: + do_throw("unexpected attempt number " + attempt); + return 1; + } +} + +function promptPasswordPS( + aParent, + aDialogTitle, + aText, + aPassword, + aCheckMsg, + aCheckState +) { + if (attempt == 2) { + aPassword.value = kValidPassword; + aCheckState.value = true; + return true; + } + return false; +} + +add_task(async function () { + function createHandler(d) { + var handler = new SMTP_RFC2821_handler(d); + handler.dropOnAuthFailure = true; + // Username needs to match the login information stored in the signons json + // file. + handler.kUsername = kUsername; + handler.kPassword = kValidPassword; + handler.kAuthRequired = true; + handler.kAuthSchemes = ["PLAIN", "LOGIN"]; // make match expected transaction below + return handler; + } + server = setupServerDaemon(createHandler); + + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-mailnews1.8.json"); + + registerAlertTestUtils(); + + // Test file + var testFile = do_get_file("data/message1.eml"); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + // Start the fake SMTP server + server.start(); + var smtpServer = getBasicSmtpServer(server.port); + var identity = getSmtpIdentity(kIdentityMail, smtpServer); + + // This time with auth + test = "Auth sendMailMessage"; + + smtpServer.authMethod = Ci.nsMsgAuthMethod.passwordCleartext; + smtpServer.socketType = Ci.nsMsgSocketType.plain; + smtpServer.username = kUsername; + + do_test_pending(); + + MailServices.smtp.sendMailMessage( + testFile, + kTo, + identity, + kSender, + null, + URLListener, + null, + null, + false, + "", + {}, + {} + ); + + server.performTest(); +}); + +var URLListener = { + OnStartRunningUrl(url) {}, + OnStopRunningUrl(url, rc) { + // Check for ok status. + Assert.equal(rc, 0); + // Now check the new password has been saved. + let logins = Services.logins.findLogins( + "smtp://localhost", + null, + "smtp://localhost" + ); + + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, kUsername); + Assert.equal(logins[0].password, kValidPassword); + + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + do_test_finished(); + }, +}; diff --git a/comm/mailnews/compose/test/unit/test_smtpProtocols.js b/comm/mailnews/compose/test/unit/test_smtpProtocols.js new file mode 100644 index 0000000000..bba7d55b6b --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpProtocols.js @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for getting smtp urls via the protocol handler. + */ + +var defaultProtocolFlags = + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD | + Ci.nsIProtocolHandler.URI_NON_PERSISTABLE | + Ci.nsIProtocolHandler.ALLOWS_PROXY | + Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT; + +var protocols = [ + { + protocol: "smtp", + urlSpec: "smtp://user@localhost/", + defaultPort: Ci.nsISmtpUrl.DEFAULT_SMTP_PORT, + }, + { + protocol: "smtps", + urlSpec: "smtps://user@localhost/", + defaultPort: Ci.nsISmtpUrl.DEFAULT_SMTPS_PORT, + }, +]; + +function run_test() { + for (var part = 0; part < protocols.length; ++part) { + print("protocol: " + protocols[part].protocol); + + var pH = Cc[ + "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol + ].createInstance(Ci.nsIProtocolHandler); + + Assert.equal(pH.scheme, protocols[part].protocol); + Assert.equal( + Services.io.getDefaultPort(pH.scheme), + protocols[part].defaultPort + ); + Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags); + + // Whip through some of the ports to check we get the right results. + for (let i = 0; i < 1024; ++i) { + Assert.equal(pH.allowPort(i, ""), i == protocols[part].defaultPort); + } + + // Check we get a URI when we ask for one + var uri = Services.io.newURI(protocols[part].urlSpec); + + uri.QueryInterface(Ci.nsISmtpUrl); + + Assert.equal(uri.spec, protocols[part].urlSpec); + + try { + // This call should throw NS_ERROR_NOT_IMPLEMENTED. If it doesn't, + // then we should implement a new test for it. + pH.newChannel(uri, null); + // If it didn't throw, then shout about it. + do_throw("newChannel not throwing NS_ERROR_NOT_IMPLEMENTED."); + } catch (ex) { + Assert.equal(ex.result, Cr.NS_ERROR_NOT_IMPLEMENTED); + } + } +} diff --git a/comm/mailnews/compose/test/unit/test_smtpProxy.js b/comm/mailnews/compose/test/unit/test_smtpProxy.js new file mode 100644 index 0000000000..7a008be001 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpProxy.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +// Tests that SMTP over a SOCKS proxy works. + +const { NetworkTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/NetworkTestUtils.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +const PORT = 25; +var daemon, localserver, server; + +add_setup(function () { + localAccountUtils.loadLocalMailAccount(); + server = setupServerDaemon(); + daemon = server._daemon; + server.start(); + NetworkTestUtils.configureProxy("smtp.tinderbox.invalid", PORT, server.port); + localserver = getBasicSmtpServer(PORT, "smtp.tinderbox.invalid"); +}); + +add_task(async function sendMessage() { + equal(daemon.post, undefined); + let identity = getSmtpIdentity("test@tinderbox.invalid", localserver); + var testFile = do_get_file("data/message1.eml"); + var urlListener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.smtp.sendMailMessage( + testFile, + "somebody@example.org", + identity, + "me@example.org", + null, + urlListener, + null, + null, + false, + "", + {}, + {} + ); + await urlListener.promise; + notEqual(daemon.post, ""); +}); + +add_task(async function cleanUp() { + NetworkTestUtils.shutdownServers(); +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpServer.js b/comm/mailnews/compose/test/unit/test_smtpServer.js new file mode 100644 index 0000000000..5e252a44f0 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpServer.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for nsISmtpServer implementation. + */ + +/** + * Test that cached server password is cleared when password storage changed. + */ +add_task(async function test_passwordmgr_change() { + // Create an nsISmtpServer instance and set a password. + let server = Cc["@mozilla.org/messenger/smtp/server;1"].createInstance( + Ci.nsISmtpServer + ); + server.password = "smtp-pass"; + equal(server.password, "smtp-pass", "Password should be cached."); + + // Trigger the change event of password manager. + Services.logins.setLoginSavingEnabled("smtp://localhost", false); + equal(server.password, "", "Password should be cleared."); +}); + +/** + * Test getter/setter of attributes. + */ +add_task(async function test_attributes() { + // Create an nsISmtpServer instance and set a password. + let server = Cc["@mozilla.org/messenger/smtp/server;1"].createInstance( + Ci.nsISmtpServer + ); + + server.description = "アイウ"; + equal(server.description, "アイウ", "Description should be correctly set."); + + server.hostname = "サービス.jp"; + equal(server.hostname, "サービス.jp", "Hostname should be correctly set."); +}); + +/** + * Tests the UID attribute of servers. + */ +add_task(async function testUID() { + const UUID_REGEXP = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + + // Create a server and check it the UID is set when accessed. + + let serverA = MailServices.smtp.createServer(); + Assert.stringMatches( + serverA.UID, + UUID_REGEXP, + "server A's UID should exist and be a UUID" + ); + Assert.equal( + Services.prefs.getStringPref(`mail.smtpserver.${serverA.key}.uid`), + serverA.UID, + "server A's UID should be saved to the preferences" + ); + Assert.throws( + () => (serverA.UID = "00001111-2222-3333-4444-555566667777"), + /NS_ERROR_ABORT/, + "server A's UID should be unchangeable after it is set" + ); + + // Create a second server and check the two UIDs don't match. + + let serverB = MailServices.smtp.createServer(); + Assert.stringMatches( + serverB.UID, + UUID_REGEXP, + "server B's UID should exist and be a UUID" + ); + Assert.equal( + Services.prefs.getStringPref(`mail.smtpserver.${serverB.key}.uid`), + serverB.UID, + "server B's UID should be saved to the preferences" + ); + Assert.notEqual( + serverB.UID, + serverA.UID, + "server B's UID should not be the same as server A's" + ); + + // Create a third server and set the UID before it is accessed. + + let serverC = MailServices.smtp.createServer(); + serverC.UID = "11112222-3333-4444-5555-666677778888"; + Assert.equal( + serverC.UID, + "11112222-3333-4444-5555-666677778888", + "server C's UID set correctly" + ); + Assert.equal( + Services.prefs.getStringPref(`mail.smtpserver.${serverC.key}.uid`), + "11112222-3333-4444-5555-666677778888", + "server C's UID should be saved to the preferences" + ); + Assert.throws( + () => (serverC.UID = "22223333-4444-5555-6666-777788889999"), + /NS_ERROR_ABORT/, + "server C's UID should be unchangeable after it is set" + ); +}); diff --git a/comm/mailnews/compose/test/unit/test_smtpURL.js b/comm/mailnews/compose/test/unit/test_smtpURL.js new file mode 100644 index 0000000000..833ca91817 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_smtpURL.js @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for checking SMTP URLs are working as expected. + * XXX this test needs extending as we fix up nsSmtpUrl. + */ + +var smtpURLs = [ + { + url: "smtp://user@localhost/", + spec: "smtp://user@localhost/", + username: "user", + }, + { + url: "smtps://user@localhost/", + spec: "smtps://user@localhost/", + username: "user", + }, +]; + +function run_test() { + var url; + for (var part = 0; part < smtpURLs.length; ++part) { + print("url: " + smtpURLs[part].url); + + url = Services.io.newURI(smtpURLs[part].url); + + Assert.equal(url.spec, smtpURLs[part].spec); + Assert.equal(url.username, smtpURLs[part].username); + } +} diff --git a/comm/mailnews/compose/test/unit/test_splitRecipients.js b/comm/mailnews/compose/test/unit/test_splitRecipients.js new file mode 100644 index 0000000000..b51da1c7a2 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_splitRecipients.js @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsMsgCompFields functions. + * Currently only tests nsIMsgCompFields::SplitRecipients + */ + +var splitRecipientsTests = [ + { + recipients: "me@foo.invalid", + emailAddressOnly: false, + count: 1, + result: ["me@foo.invalid"], + }, + { + recipients: "me@foo.invalid, me2@foo.invalid", + emailAddressOnly: false, + count: 2, + result: ["me@foo.invalid", "me2@foo.invalid"], + }, + { + recipients: '"foo bar" <me@foo.invalid>', + emailAddressOnly: false, + count: 1, + result: ["foo bar <me@foo.invalid>"], + }, + { + recipients: '"foo bar" <me@foo.invalid>', + emailAddressOnly: true, + count: 1, + result: ["me@foo.invalid"], + }, + { + recipients: '"foo bar" <me@foo.invalid>, "bar foo" <me2@foo.invalid>', + emailAddressOnly: false, + count: 2, + result: ["foo bar <me@foo.invalid>", "bar foo <me2@foo.invalid>"], + }, + { + recipients: '"foo bar" <me@foo.invalid>, "bar foo" <me2@foo.invalid>', + emailAddressOnly: true, + count: 2, + result: ["me@foo.invalid", "me2@foo.invalid"], + }, + { + recipients: + "A Group:Ed Jones <c@a.invalid>,joe@where.invalid,John <jdoe@one.invalid>;", + emailAddressOnly: false, + count: 3, + result: [ + "Ed Jones <c@a.invalid>", + "joe@where.invalid", + "John <jdoe@one.invalid>", + ], + }, + { + recipients: + "mygroup:;, empty:;, foo@foo.invalid, othergroup:bar@foo.invalid, bar2@foo.invalid;, y@y.invalid, empty:;", + emailAddressOnly: true, + count: 4, + result: [ + "foo@foo.invalid", + "bar@foo.invalid", + "bar2@foo.invalid", + "y@y.invalid", + ], + }, + { + recipients: "Undisclosed recipients:;;;;;;;;;;;;;;;;,,,,,,,,,,,,,,,,", + emailAddressOnly: true, + count: 0, + result: [], + }, + { + recipients: "a@xxx.invalid; b@xxx.invalid", + emailAddressOnly: true, + count: 2, + result: ["a@xxx.invalid", "b@xxx.invalid"], + }, + { + recipients: "a@xxx.invalid; B <b@xxx.invalid>", + emailAddressOnly: false, + count: 2, + result: ["a@xxx.invalid", "B <b@xxx.invalid>"], + }, + { + recipients: '"A " <a@xxx.invalid>; b@xxx.invalid', + emailAddressOnly: false, + count: 2, + result: ["A <a@xxx.invalid>", "b@xxx.invalid"], + }, + { + recipients: "A <a@xxx.invalid>; B <b@xxx.invalid>", + emailAddressOnly: false, + count: 2, + result: ["A <a@xxx.invalid>", "B <b@xxx.invalid>"], + }, + { + recipients: + "A (this: is, a comment;) <a.invalid>; g: (this: is, <a> comment;) C <c.invalid>, d.invalid;", + emailAddressOnly: false, + count: 3, + result: [ + "A (this: is, a comment;) <a.invalid>", + "(this: is, <a> comment;) C <c.invalid>", + "d.invalid <>", + ], + }, + { + recipients: + 'Mary Smith <mary@x.invalid>, extra:;, group:jdoe@example.invalid; Who? <one@y.invalid>; <boss@nil.invalid>, "Giant; \\"Big\\" Box" <sysservices@example.invalid>, ', + emailAddressOnly: false, + count: 5, + result: [ + "Mary Smith <mary@x.invalid>", + "jdoe@example.invalid", + "Who? <one@y.invalid>", + "boss@nil.invalid", + 'Giant; "Big" Box <sysservices@example.invalid>', + ], + }, + { + recipients: "Undisclosed recipients: a@foo.invalid ;;extra:;", + emailAddressOnly: true, + count: 1, + result: ["a@foo.invalid"], + }, + { + recipients: "Undisclosed recipients:;;extra:a@foo.invalid;", + emailAddressOnly: true, + count: 1, + result: ["a@foo.invalid"], + }, + { + recipients: "", + emailAddressOnly: false, + count: 0, + result: [], + }, +]; + +function run_test() { + var fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // As most of SplitRecipients functionality is in the nsIMsgHeaderParser + // functionality, here (at least initially), we're just interested in checking + // the basic argument/return combinations. + + for (var part = 0; part < splitRecipientsTests.length; ++part) { + print("Test: " + splitRecipientsTests[part].recipients); + var result = fields.splitRecipients( + splitRecipientsTests[part].recipients, + splitRecipientsTests[part].emailAddressOnly + ); + + Assert.equal(splitRecipientsTests[part].count, result.length); + + for (var item = 0; item < result.length; ++item) { + Assert.equal(splitRecipientsTests[part].result[item], result[item]); + } + } +} diff --git a/comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js b/comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js new file mode 100644 index 0000000000..427c101914 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_staleTemporaryFileCleanup.js @@ -0,0 +1,57 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/* + * Test that stale temporary files are cleaned up when the msg compose service + * is initialized. + */ + +var gExpectedFiles; + +function create_temporary_files_for(name) { + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append(name); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + return file; +} + +function collect_expected_temporary_files() { + let files = []; + + files.push(create_temporary_files_for("nsmail.tmp")); + files.push(create_temporary_files_for("nsmail.tmp")); + files.push(create_temporary_files_for("nsmail.tmp")); + files.push(create_temporary_files_for("nsemail.eml")); + files.push(create_temporary_files_for("nsemail.tmp")); + files.push(create_temporary_files_for("nsqmail.tmp")); + files.push(create_temporary_files_for("nscopy.tmp")); + files.push(create_temporary_files_for("nscopy.tmp")); + + return files; +} + +function check_files_not_exist(files) { + files.forEach(function (file) { + Assert.ok(!file.exists()); + }); +} + +function run_test() { + gExpectedFiles = collect_expected_temporary_files(); + registerCleanupFunction(function () { + gExpectedFiles.forEach(function (file) { + if (file.exists()) { + file.remove(false); + } + }); + }); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + MailServices.compose; // Initialise the compose service. + do_test_pending(); + check_files_not_exist(gExpectedFiles); + do_test_finished(); +} diff --git a/comm/mailnews/compose/test/unit/test_telemetry_compose.js b/comm/mailnews/compose/test/unit/test_telemetry_compose.js new file mode 100644 index 0000000000..f48db07293 --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_telemetry_compose.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test telemetry related to message composition. + */ + +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); + +const HTML_SCALAR = "tb.compose.format_html"; +const PLAIN_TEXT_SCALAR = "tb.compose.format_plain_text"; + +/** + * Check that we're counting HTML or Plain text when composing. + */ +add_task(async function test_compose_format() { + Services.telemetry.clearScalars(); + + // Bare-bones code to initiate composing a message in given format. + let createCompose = function (fmt) { + let msgCompose = Cc[ + "@mozilla.org/messengercompose/compose;1" + ].createInstance(Ci.nsIMsgCompose); + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + + params.format = fmt; + msgCompose.initialize(params); + }; + + // Start composing arbitrary numbers of messages in each format. + const NUM_HTML = 7; + const NUM_PLAIN = 13; + for (let i = 0; i < NUM_HTML; i++) { + createCompose(Ci.nsIMsgCompFormat.HTML); + } + for (let i = 0; i < NUM_PLAIN; i++) { + createCompose(Ci.nsIMsgCompFormat.PlainText); + } + + // Did we count them correctly? + const scalars = TelemetryTestUtils.getProcessScalars("parent"); + Assert.equal( + scalars[HTML_SCALAR], + NUM_HTML, + HTML_SCALAR + " must have the correct value." + ); + Assert.equal( + scalars[PLAIN_TEXT_SCALAR], + NUM_PLAIN, + PLAIN_TEXT_SCALAR + " must have the correct value." + ); +}); + +/** + * Check that we're counting compose type (new/reply/fwd etc) when composing. + */ +add_task(async function test_compose_type() { + // Bare-bones code to initiate composing a message in given type. + let createCompose = function (type) { + let msgCompose = Cc[ + "@mozilla.org/messengercompose/compose;1" + ].createInstance(Ci.nsIMsgCompose); + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + + params.type = type; + msgCompose.initialize(params); + }; + const histogram = TelemetryTestUtils.getAndClearHistogram("TB_COMPOSE_TYPE"); + + // Start composing arbitrary numbers of messages in each format. + const NUM_NEW = 4; + const NUM_DRAFT = 7; + const NUM_EDIT_TEMPLATE = 3; + for (let i = 0; i < NUM_NEW; i++) { + createCompose(Ci.nsIMsgCompType.New); + } + for (let i = 0; i < NUM_DRAFT; i++) { + createCompose(Ci.nsIMsgCompType.Draft); + } + for (let i = 0; i < NUM_EDIT_TEMPLATE; i++) { + createCompose(Ci.nsIMsgCompType.EditTemplate); + } + + // Did we count them correctly? + const snapshot = histogram.snapshot(); + Assert.equal( + snapshot.values[Ci.nsIMsgCompType.New], + NUM_NEW, + "nsIMsgCompType.New count must be correct" + ); + Assert.equal( + snapshot.values[Ci.nsIMsgCompType.Draft], + NUM_DRAFT, + "nsIMsgCompType.Draft count must be correct" + ); + Assert.equal( + snapshot.values[Ci.nsIMsgCompType.EditTemplate], + NUM_EDIT_TEMPLATE, + "nsIMsgCompType.EditTemplate count must be correct" + ); +}); diff --git a/comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js b/comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js new file mode 100644 index 0000000000..6a350ac64e --- /dev/null +++ b/comm/mailnews/compose/test/unit/test_temporaryFilesRemoved.js @@ -0,0 +1,123 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/* + * Test that temporary files for draft are surely removed. + */ + +var gMsgCompose; +var gExpectedFiles; + +var progressListener = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + do_timeout(0, checkResult); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) {}, + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange(aWebProgress, aRequest, state) {}, + onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +/** + * Get the count of temporary files. Because nsIFile.createUnique creates a random + * file name, we iterate the tmp dir and count the files that match filename + * patterns. + */ +async function getTemporaryFilesCount() { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile).path; + let entries = await IOUtils.getChildren(tmpDir); + let tempFiles = { + "nsmail.tmp": 0, + "nscopy.tmp": 0, + "nsemail.eml": 0, + "nsemail.tmp": 0, + "nsqmail.tmp": 0, + }; + for (const path of entries) { + for (let pattern of Object.keys(tempFiles)) { + let [name, extName] = pattern.split("."); + if (PathUtils.filename(path).startsWith(name) && path.endsWith(extName)) { + tempFiles[pattern]++; + } + } + } + return tempFiles; +} + +/** + * Temp files should be deleted as soon as the draft is finished saving, so the + * counts should be the same as before. + */ +async function checkResult() { + let filesCount = await getTemporaryFilesCount(); + for (let [pattern, count] of Object.entries(filesCount)) { + Assert.equal( + count, + gExpectedFiles[pattern], + `${pattern} should not exists` + ); + } + do_test_finished(); +} + +add_task(async function () { + gExpectedFiles = await getTemporaryFilesCount(); + + // Ensure we have at least one mail account + localAccountUtils.loadLocalMailAccount(); + + gMsgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + + fields.from = "Nobody <nobody@tinderbox.test>"; + fields.body = "body text"; + fields.useMultipartAlternative = true; + + params.composeFields = fields; + params.format = Ci.nsIMsgCompFormat.HTML; + + gMsgCompose.initialize(params, null, null); + + let identity = getSmtpIdentity(null, getBasicSmtpServer()); + + localAccountUtils.rootFolder.createLocalSubfolder("Drafts"); + + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + progress.registerListener(progressListener); + + do_test_pending(); + + gMsgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgSaveAsDraft, + identity, + "", + null, + progress + ); +}); diff --git a/comm/mailnews/compose/test/unit/xpcshell.ini b/comm/mailnews/compose/test/unit/xpcshell.ini new file mode 100644 index 0000000000..687259c42a --- /dev/null +++ b/comm/mailnews/compose/test/unit/xpcshell.ini @@ -0,0 +1,54 @@ +[DEFAULT] +head = head_compose.js +tail = +support-files = data/* + +[test_accountKey.js] +[test_attachment.js] +[test_attachment_intl.js] +[test_autoReply.js] +skip-if = os == 'mac' +[test_bcc.js] +[test_bug155172.js] +[test_bug474774.js] +[test_createAndSendMessage.js] +[test_createRFC822Message.js] +[test_detectAttachmentCharset.js] +[test_expandMailingLists.js] +[test_fcc2.js] +[test_fccReply.js] +[test_longLines.js] +[test_mailTelemetry.js] +[test_mailtoURL.js] +[test_messageBody.js] +[test_messageHeaders.js] +[test_nsIMsgCompFields.js] +[test_nsMsgCompose1.js] +[test_nsMsgCompose2.js] +[test_nsMsgCompose3.js] +[test_nsSmtpService1.js] +[test_saveDraft.js] +[test_sendBackground.js] +[test_sendMailAddressIDN.js] +[test_sendMailMessage.js] +[test_sendMessageFile.js] +[test_sendMessageLater.js] +[test_sendMessageLater2.js] +[test_sendMessageLater3.js] +[test_sendObserver.js] +[test_smtp8bitMime.js] +[test_smtpAuthMethods.js] +[test_smtpClient.js] +[test_smtpPassword.js] +[test_smtpPassword2.js] +[test_smtpPasswordFailure1.js] +[test_smtpPasswordFailure2.js] +[test_smtpPasswordFailure3.js] +[test_smtpProtocols.js] +[test_smtpProxy.js] +[test_smtpServer.js] +[test_smtpURL.js] +[test_splitRecipients.js] +[test_telemetry_compose.js] +[test_staleTemporaryFileCleanup.js] +[test_temporaryFilesRemoved.js] |