Bug 1211292 - Enable test_saveDraft and test_detectAttachmentCharset for MessageSend.jsm. r=mkmelin DONTBUILD
authorPing Chen <remotenonsense@gmail.com>
Mon, 07 Sep 2020 13:54:52 +0300
changeset 39776 5fafcb896ef778e0131969af4847d8aad20a66e9
parent 39775 41824ba64913f4acad854f3aab30d0b4cb14ad4c
child 39777 08722934183db9f1e0e60a14f4d115af68f8164b
push id2763
push userthunderbird@calypsoblue.org
push dateMon, 21 Sep 2020 19:32:17 +0000
treeherdercomm-beta@ea5933f4b0c8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1211292
Bug 1211292 - Enable test_saveDraft and test_detectAttachmentCharset for MessageSend.jsm. r=mkmelin DONTBUILD
mailnews/compose/public/nsIMsgCompUtils.idl
mailnews/compose/src/MessageSend.jsm
mailnews/compose/src/MimeMessageUtils.jsm
mailnews/compose/src/MimePart.jsm
mailnews/compose/src/nsMsgCompUtils.cpp
mailnews/compose/test/unit/xpcshell-cpp.ini
mailnews/compose/test/unit/xpcshell-shared.ini
--- a/mailnews/compose/public/nsIMsgCompUtils.idl
+++ b/mailnews/compose/public/nsIMsgCompUtils.idl
@@ -6,9 +6,20 @@
 #include "nsISupports.idl"
 #include "nsIMsgIdentity.idl"
 
 [scriptable, uuid(00b4569a-077e-4236-b993-980fd82bb948)]
 interface nsIMsgCompUtils : nsISupports {
   string mimeMakeSeparator(in string prefix);
   string msgGenerateMessageId(in nsIMsgIdentity identity);
   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);
 };
--- a/mailnews/compose/src/MessageSend.jsm
+++ b/mailnews/compose/src/MessageSend.jsm
@@ -1,20 +1,21 @@
 /* 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"];
 
-let { MailServices } = ChromeUtils.import(
+var { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+var { MailServices } = ChromeUtils.import(
   "resource:///modules/MailServices.jsm"
 );
-let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-let { MimeMessage } = ChromeUtils.import("resource:///modules/MimeMessage.jsm");
-let { MsgUtils } = ChromeUtils.import(
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { MimeMessage } = ChromeUtils.import("resource:///modules/MimeMessage.jsm");
+var { MsgUtils } = ChromeUtils.import(
   "resource:///modules/MimeMessageUtils.jsm"
 );
 
 /**
  * A work in progress rewriting of nsMsgSend.cpp.
  * Set `user_pref("mailnews.send.jsmodule", true);` to use this module.
  *
  * @implements {nsIMsgSend}
@@ -124,16 +125,30 @@ MessageSend.prototype = {
   },
 
   notifyListenerOnStartSending(msgId, msgSize) {
     if (this._sendListener) {
       this._sendListener.onStartSending(msgId, msgSize);
     }
   },
 
+  notifyListenerOnStartCopy() {
+    let copyListener = this._sendListener.QueryInterface(
+      Ci.nsIMsgCopyServiceListener
+    );
+    copyListener.OnStartCopy();
+  },
+
+  notifyListenerOnProgressCopy(progress, progressMax) {
+    let copyListener = this._sendListener.QueryInterface(
+      Ci.nsIMsgCopyServiceListener
+    );
+    copyListener.OnProgress(progress, progressMax);
+  },
+
   notifyListenerOnStopCopy(status) {
     if (this._sendListener) {
       let copyListener = this._sendListener.QueryInterface(
         Ci.nsIMsgCopyServiceListener
       );
       copyListener.OnStopCopy(status);
     }
   },
@@ -177,65 +192,90 @@ MessageSend.prototype = {
   /**
    * Create a local file from MimeMessage, then pass it to _deliverMessage.
    */
   async _createAndSendMessage() {
     this._setStatusMessage(
       this._composeBundle.GetStringFromName("creatingMailMessage")
     );
     let messageFile = await this._message.createMessageFile();
-    this._deliverMessage(messageFile);
+    await this._deliverMessage(messageFile);
   },
 
   _setStatusMessage(msg) {
     if (this._sendProgress) {
       this._sendProgress.onStatusChange(null, null, Cr.NS_OK, msg);
     }
   },
 
   /**
    * Deliver a message. Far from complete.
    *
    * @param {nsIFile} file - The message file to deliver.
    */
-  _deliverMessage(file) {
+  async _deliverMessage(file) {
     if (
       [
         Ci.nsIMsgSend.nsMsgQueueForLater,
         Ci.nsIMsgSend.nsMsgDeliverBackground,
         Ci.nsIMsgSend.nsMsgSaveAsDraft,
         Ci.nsIMsgSend.nsMsgSaveAsTemplate,
       ].includes(this._deliverMode)
     ) {
-      this._sendToMagicFolder(file);
+      await this._sendToMagicFolder(file);
       return;
     }
     this._deliverFileAsMail(file);
     this._sendToMagicFolder(file);
   },
 
   /**
    * Copy a message to Draft/Sent or other folder depending on pref and
    * deliverMode.
    *
    * @param {nsIFile} file - The message file to copy.
    */
-  _sendToMagicFolder(file) {
+  async _sendToMagicFolder(file) {
     let folderUri = MsgUtils.getMsgFolderURIFromPrefs(
       this._userIdentity,
       this._deliverMode
     );
     let msgCopy = Cc["@mozilla.org/messengercompose/msgcopy;1"].createInstance(
       Ci.nsIMsgCopy
     );
+    let copyFile = file;
+    if (folderUri.startsWith("mailbox:")) {
+      // Add a `From -` line, so that nsLocalMailFolder.cpp won't add a dummy
+      // envelope.
+      let { path, file: fileWriter } = await OS.File.openUnique(
+        OS.Path.join(OS.Constants.Path.tmpDir, "nscopy.eml")
+      );
+      await fileWriter.write(new TextEncoder().encode("From -\r\n"));
+      let xMozillaStatus = MsgUtils.getXMozillaStatus(this._deliverMode);
+      let xMozillaStatus2 = MsgUtils.getXMozillaStatus2(this._deliverMode);
+      if (xMozillaStatus) {
+        await fileWriter.write(
+          new TextEncoder().encode(`X-Mozilla-Status: ${xMozillaStatus}\r\n`)
+        );
+      }
+      if (xMozillaStatus2) {
+        await fileWriter.write(
+          new TextEncoder().encode(`X-Mozilla-Status2: ${xMozillaStatus2}\r\n`)
+        );
+      }
+      await fileWriter.write(await OS.File.read(file.path));
+      await fileWriter.close();
+      copyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+      copyFile.initWithPath(path);
+    }
     // Notify nsMsgCompose about the saved folder.
     this._sendListener.onGetDraftFolderURI(folderUri);
     msgCopy.startCopyOperation(
       this._userIdentity,
-      file,
+      copyFile,
       this._deliverMode,
       this,
       folderUri,
       this._msgToReplace
     );
   },
 
   /**
--- a/mailnews/compose/src/MimeMessageUtils.jsm
+++ b/mailnews/compose/src/MimeMessageUtils.jsm
@@ -294,16 +294,82 @@ var MsgUtils = {
       value += `; provider=${attachment.cloudFileAccountKey}`;
       value += `; file=${attachment.url}`;
     }
     value += `; name=${attachment.name}`;
     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().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().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 &&
@@ -491,23 +557,49 @@ var MsgUtils = {
     let value = "";
     for (let char of baseUrl) {
       value += transformMap[char] || char;
     }
     return value;
   },
 
   /**
-   * TODO: Pick the charset according to attachment content.
+   * Pick a charset according to content type and content.
+   * @param {string} contentType - The content type.
+   * @param {string} content - The content.
+   * @returns {string}
    */
   pickCharset(contentType, content) {
-    if (contentType.startsWith("text")) {
-      return "UTF-8";
+    if (!contentType.startsWith("text")) {
+      return "";
     }
-    return "";
+
+    // Check the BOM.
+    let charset = "";
+    if (content.length >= 2) {
+      let byte0 = content.charCodeAt(0);
+      let byte1 = content.charCodeAt(1);
+      let byte2 = content.charCodeAt(2);
+      if (byte0 == 0xfe && byte1 == 0xff) {
+        charset = "UTF-16BE";
+      } else if (byte0 == 0xff && byte1 == 0xfe) {
+        charset = "UTF-16LE";
+      } else if (byte0 == 0xef && byte1 == 0xbb && byte2 == 0xbf) {
+        charset = "UTF-8";
+      }
+    }
+    if (charset) {
+      return charset;
+    }
+
+    // Use mozilla::EncodingDetector.
+    let compUtils = Cc[
+      "@mozilla.org/messengercompose/computils;1"
+    ].createInstance(Ci.nsIMsgCompUtils);
+    return compUtils.detectCharset(content);
   },
 
   /**
    * Given a string, convert it to 'qtext' (quoted text) for RFC822 header
    * purposes.
    */
   makeFilenameQtext(srcText, stripCRLFs) {
     let size = srcText.length;
--- a/mailnews/compose/src/MimePart.jsm
+++ b/mailnews/compose/src/MimePart.jsm
@@ -157,18 +157,19 @@ class MimePart {
     }
 
     return content;
   }
 
   /**
    * Recursively write a MimePart and its parts to a file.
    * @param {OS.File} file - The output file to contain a RFC2045 message.
+   * @param {number} [depth=0] - Nested level of a part.
    */
-  async write(file) {
+  async write(file, depth = 0) {
     this._outFile = file;
     let bodyString = this._bodyText;
     // If this is an attachment part, use the attachment content as bodyString.
     if (this._bodyAttachment) {
       bodyString = await this.fetchFile();
     }
     if (bodyString) {
       let encoder = new MimeEncoder(
@@ -191,27 +192,33 @@ class MimePart {
         useASCII: true,
       })
     );
 
     // Recursively write out parts.
     if (this._parts.length) {
       // single part message
       if (!this._separator && this._parts.length === 1) {
-        await this._parts[0].write(file);
+        await this._parts[0].write(file, depth + 1);
         await this._writeString(`${bodyString}\r\n`);
         return;
       }
 
       await this._writeString("\r\n");
+      if (depth == 0) {
+        // Current part is a top part and multipart container.
+        await this._writeString(
+          "This is a multi-part message in MIME format.\r\n"
+        );
+      }
 
       // multipart message
       for (let part of this._parts) {
         await this._writeString(`--${this._separator}\r\n`);
-        await part.write(file);
+        await part.write(file, depth + 1);
       }
       await this._writeString(`--${this._separator}--\r\n`);
     }
 
     // Write out body.
     await this._writeString(`\r\n${bodyString}\r\n`);
   }
 
@@ -229,11 +236,14 @@ class MimePart {
   _makePartSeparator() {
     return (
       "------------" +
       btoa(
         String.fromCharCode(
           ...[...Array(18)].map(() => Math.floor(Math.random() * 256))
         )
       )
+        // Boundary is used to construct RegExp in tests, + would break those
+        // tests.
+        .replaceAll("+", "-")
     );
   }
 }
--- a/mailnews/compose/src/nsMsgCompUtils.cpp
+++ b/mailnews/compose/src/nsMsgCompUtils.cpp
@@ -26,17 +26,19 @@
 #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/mailnews/Services.h"
+#include "mozilla/EncodingDetector.h"
 #include "mozilla/Services.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"
@@ -64,16 +66,29 @@ NS_IMETHODIMP nsMsgCompUtils::MsgGenerat
 }
 
 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 =
--- a/mailnews/compose/test/unit/xpcshell-cpp.ini
+++ b/mailnews/compose/test/unit/xpcshell-cpp.ini
@@ -3,29 +3,26 @@ head = head_compose.js
 tail =
 dupe-manifest =
 support-files = data/*
 
 [test_attachment.js]
 [test_attachment_intl.js]
 [test_autoReply.js]
 skip-if = os == 'mac'
-[test_bug155172.js]
 [test_bug235432.js]
 [test_bug474774.js]
-[test_detectAttachmentCharset.js]
 [test_expandMailingLists.js]
 [test_mailtoURL.js]
 [test_nsIMsgCompFields.js]
 [test_nsMsgCompose1.js]
 [test_nsMsgCompose2.js]
 [test_nsMsgCompose3.js]
 [test_nsMsgCompose4.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]
@@ -37,13 +34,10 @@ skip-if = os == 'mac'
 [test_smtpPasswordFailure2.js]
 [test_smtpPasswordFailure3.js]
 [test_smtpProtocols.js]
 [test_smtpProxy.js]
 [test_smtpURL.js]
 [test_splitRecipients.js]
 [test_staleTemporaryFileCleanup.js]
 [test_temporaryFilesRemoved.js]
-[test_longLines.js]
-[test_telemetry_compose.js]
-[test_mailTelemetry.js]
 
 [include:xpcshell-shared.ini]
\ No newline at end of file
--- a/mailnews/compose/test/unit/xpcshell-shared.ini
+++ b/mailnews/compose/test/unit/xpcshell-shared.ini
@@ -1,1 +1,7 @@
-[test_messageHeaders.js]
\ No newline at end of file
+[test_bug155172.js]
+[test_detectAttachmentCharset.js]
+[test_longLines.js]
+[test_mailTelemetry.js]
+[test_messageHeaders.js]
+[test_saveDraft.js]
+[test_telemetry_compose.js]
\ No newline at end of file