Bug 1211292 - Enable test_messageHeaders for MessageSend.jsm. r=mkmelin
authorPing Chen <remotenonsense@gmail.com>
Mon, 07 Sep 2020 13:54:34 +0300
changeset 39775 41824ba64913f4acad854f3aab30d0b4cb14ad4c
parent 39774 1a13d435f8ae7495fc32d0a95e42cc2e7a39cf0d
child 39776 5fafcb896ef778e0131969af4847d8aad20a66e9
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_messageHeaders for MessageSend.jsm. r=mkmelin
mailnews/compose/public/moz.build
mailnews/compose/public/nsIMsgCopy.idl
mailnews/compose/src/MessageSend.jsm
mailnews/compose/src/MimeEncoder.jsm
mailnews/compose/src/MimeMessage.jsm
mailnews/compose/src/MimeMessageUtils.jsm
mailnews/compose/src/MimePart.jsm
mailnews/compose/src/components.conf
mailnews/compose/src/moz.build
mailnews/compose/src/nsMsgCompUtils.cpp
mailnews/compose/src/nsMsgCopy.cpp
mailnews/compose/src/nsMsgCopy.h
mailnews/compose/src/nsMsgSend.cpp
mailnews/compose/test/moz.build
mailnews/compose/test/unit/head_compose_jsm.js
mailnews/compose/test/unit/test_messageHeaders.js
mailnews/compose/test/unit/xpcshell-cpp.ini
mailnews/compose/test/unit/xpcshell-jsm.ini
mailnews/compose/test/unit/xpcshell-shared.ini
mailnews/compose/test/unit/xpcshell.ini
mailnews/mime/jsmime/jsmime.js
mailnews/mime/src/MimeJSComponents.jsm
--- a/mailnews/compose/public/moz.build
+++ b/mailnews/compose/public/moz.build
@@ -8,16 +8,17 @@ XPIDL_SOURCES += [
     'nsIMsgAttachmentHandler.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',
@@ -27,9 +28,8 @@ XPIDL_SOURCES += [
 ]
 
 XPIDL_MODULE = 'msgcompose'
 
 EXPORTS += [
     'nsMsgAttachmentData.h',
     'nsMsgCompCID.h',
 ]
-
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/public/nsIMsgCopy.idl
@@ -0,0 +1,32 @@
+/* -*- 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 "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);
+};
--- a/mailnews/compose/src/MessageSend.jsm
+++ b/mailnews/compose/src/MessageSend.jsm
@@ -4,16 +4,19 @@
 
 const EXPORTED_SYMBOLS = ["MessageSend"];
 
 let { 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(
+  "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}
  */
 function MessageSend() {}
@@ -24,54 +27,67 @@ MessageSend.prototype = {
 
   createAndSendMessage(
     editor,
     userIdentity,
     accountKey,
     compFields,
     isDigest,
     dontDeliver,
-    mode,
+    deliverMode,
     msgToReplace,
     bodyType,
     body,
     attachments,
     preloadedAttachments,
     parentWindow,
     progress,
     listener,
     smtpPassword,
     originalMsgURI,
     type
   ) {
+    this._userIdentity = userIdentity;
     this._compFields = compFields;
-    this._userIdentity = userIdentity;
+    this._deliverMode = deliverMode;
+    this._msgToReplace = msgToReplace;
     this._sendProgress = progress;
-    this._smtpSmtpPassword = smtpPassword;
+    this._smtpPassword = smtpPassword;
     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();
-    this.sendReport.deliveryMode = mode;
+    this.sendReport.deliveryMode = deliverMode;
     this._setStatusMessage(
       this._composeBundle.GetStringFromName("assemblingMailInformation")
     );
     this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage;
 
     this._setStatusMessage(
       this._composeBundle.GetStringFromName("assemblingMessage")
     );
-    this._message = new MimeMessage(userIdentity, compFields, bodyType, body);
+    this._message = new MimeMessage(
+      userIdentity,
+      compFields,
+      bodyType,
+      body,
+      deliverMode,
+      originalMsgURI,
+      type
+    );
+
+    // nsMsgKey_None from MailNewsTypes.h.
+    this._messageKey = 0xffffffff;
     this._createAndSendMessage();
   },
 
   sendMessageFile(
     userIdentity,
     accountKey,
     compFields,
     sendIFile,
@@ -99,20 +115,17 @@ MessageSend.prototype = {
   getPartForDomIndex(domIndex) {
     throw Components.Exception(
       "getPartForDomIndex not implemented",
       Cr.NS_ERROR_NOT_IMPLEMENTED
     );
   },
 
   getProgress() {
-    throw Components.Exception(
-      "getProgress not implemented",
-      Cr.NS_ERROR_NOT_IMPLEMENTED
-    );
+    return this._sendProgress;
   },
 
   notifyListenerOnStartSending(msgId, msgSize) {
     if (this._sendListener) {
       this._sendListener.onStartSending(msgId, msgSize);
     }
   },
 
@@ -138,21 +151,28 @@ MessageSend.prototype = {
 
   get folderUri() {
     throw Components.Exception(
       "folderUri getter not implemented",
       Cr.NS_ERROR_NOT_IMPLEMENTED
     );
   },
 
+  /**
+   * @type {nsMsgKey}
+   */
+  set messageKey(key) {
+    this._messageKey = key;
+  },
+
+  /**
+   * @type {nsMsgKey}
+   */
   get messageKey() {
-    throw Components.Exception(
-      "messageKey getter not implemented",
-      Cr.NS_ERROR_NOT_IMPLEMENTED
-    );
+    return this._messageKey;
   },
 
   get sendReport() {
     return this._sendReport;
   },
 
   /**
    * Create a local file from MimeMessage, then pass it to _deliverMessage.
@@ -168,35 +188,74 @@ MessageSend.prototype = {
   _setStatusMessage(msg) {
     if (this._sendProgress) {
       this._sendProgress.onStatusChange(null, null, Cr.NS_OK, msg);
     }
   },
 
   /**
    * Deliver a message. Far from complete.
-   * TODO: implement saving to the Sent/Draft folder. Other details.
+   *
+   * @param {nsIFile} file - The message file to deliver.
    */
   _deliverMessage(file) {
+    if (
+      [
+        Ci.nsIMsgSend.nsMsgQueueForLater,
+        Ci.nsIMsgSend.nsMsgDeliverBackground,
+        Ci.nsIMsgSend.nsMsgSaveAsDraft,
+        Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+      ].includes(this._deliverMode)
+    ) {
+      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) {
+    let folderUri = MsgUtils.getMsgFolderURIFromPrefs(
+      this._userIdentity,
+      this._deliverMode
+    );
+    let msgCopy = Cc["@mozilla.org/messengercompose/msgcopy;1"].createInstance(
+      Ci.nsIMsgCopy
+    );
+    // Notify nsMsgCompose about the saved folder.
+    this._sendListener.onGetDraftFolderURI(folderUri);
+    msgCopy.startCopyOperation(
+      this._userIdentity,
+      file,
+      this._deliverMode,
+      this,
+      folderUri,
+      this._msgToReplace
+    );
   },
 
   /**
    * Send a message file to smtp service. Far from complete.
-   * TODO: handle cc/bcc. Other details.
+   * TODO: actually send the message to cc/bcc.
    */
   _deliverFileAsMail(file) {
-    let to = this._compFields.to || "";
+    let to = this._compFields.to || this._compFields.bcc || "";
     let deliveryListener = new MsgDeliveryListener(this, false);
     MailServices.smtp.sendMailMessage(
       file,
       to,
       this._userIdentity,
       this._compFields.from,
-      this._smtpSmtpPassword,
+      this._smtpPassword,
       deliveryListener,
       null,
       null,
       this._compFields.DSN,
       {},
       {}
     );
   },
--- a/mailnews/compose/src/MimeEncoder.jsm
+++ b/mailnews/compose/src/MimeEncoder.jsm
@@ -104,18 +104,21 @@ class MimeEncoder {
         this._contentType.startsWith("multipart")
       ) {
         encodeP = false;
       }
 
       let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
         Ci.nsICharsetConverterManager
       );
-      let isCharsetMultiByte =
-        manager.getCharsetData(this._charset, ".isMultibyte") == "true";
+      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") ||
--- a/mailnews/compose/src/MimeMessage.jsm
+++ b/mailnews/compose/src/MimeMessage.jsm
@@ -2,40 +2,61 @@
  * 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"];
 
 let { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 let { MimePart } = ChromeUtils.import("resource:///modules/MimePart.jsm");
+let { MsgUtils } = ChromeUtils.import(
+  "resource:///modules/MimeMessageUtils.jsm"
+);
 
 /**
- * A class to create a top MimePart and write to a tmp file.
- * Currently, only plain and/or html text without any attachments is
- * supported. It works like this:
- * 1. Collect top level MIME headers
- * 2. Construct a MimePart instance, which can be nested
- * 3. Write the MimePart to a tmp file, e.g. /tmp/nsemail.eml
+ * 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} bodyType
    * @param {string} bodyText
+   * @param {nsMsgDeliverMode} deliverMode
+   * @param {string} originalMsgURI
+   * @param {MSG_ComposeType} compType
    */
-  constructor(userIdentity, compFields, bodyType, bodyText) {
+  constructor(
+    userIdentity,
+    compFields,
+    bodyType,
+    bodyText,
+    deliverMode,
+    originalMsgURI,
+    compType
+  ) {
     this._userIdentity = userIdentity;
     this._compFields = compFields;
+    this._fcc = MsgUtils.getFcc(
+      userIdentity,
+      compFields,
+      originalMsgURI,
+      compType
+    );
     this._bodyType = bodyType;
     this._bodyText = bodyText;
+    this._deliverMode = deliverMode;
   }
 
   /**
    * Write a MimeMessage to a tmp file.
    * @returns {nsIFile}
    */
   async createMessageFile() {
     let topPart = this._initMimePart();
@@ -83,125 +104,213 @@ class MimeMessage {
 
     return topPart;
   }
 
   /**
    * Collect top level headers like From/To/Subject into a Map.
    */
   _gatherMimeHeaders() {
-    let messageId = this._compFields.getHeader("Message-Id");
+    let messageId = this._compFields.getHeader("message-id");
     if (!messageId) {
       messageId = Cc["@mozilla.org/messengercompose/computils;1"]
         .createInstance(Ci.nsIMsgCompUtils)
         .msgGenerateMessageId(this._userIdentity);
     }
     let headers = new Map([
-      ["Message-Id", messageId],
-      ["Date", new Date()],
-      ["MIME-Version", "1.0"],
+      ["message-id", messageId],
+      ["date", new Date()],
+      ["mime-version", "1.0"],
       [
-        "User-Agent",
+        "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.getHeader(headerName);
+      // The headerName is always lowercase.
+      if (
+        headerName == "bcc" &&
+        ![
+          Ci.nsIMsgSend.nsMsgQueueForLater,
+          Ci.nsIMsgSend.nsMsgSaveAsDraft,
+          Ci.nsIMsgSend.nsMsgSaveAsTemplate,
+        ].includes(this._deliverMode)
+      ) {
+        continue;
+      }
+      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));
+    }
+
+    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 {MimePart[]}
    */
   _gatherMainParts() {
-    let charset = this._compFields.characterSet;
     let formatFlowed = Services.prefs.getBoolPref(
       "mailnews.send_plaintext_flowed"
     );
-    let delsp = false;
-    let disallowBreaks = true;
-    if (charset.startsWith("ISO-2022-JP")) {
-      // Make sure we honour RFC 1468. For encoding in ISO-2022-JP we need to
-      // send short lines to allow 7bit transfer encoding.
-      disallowBreaks = false;
-      if (formatFlowed) {
-        delsp = true;
-      }
-    }
-    let charsetParams = `; charset=${charset}`;
-    let formatParams = "";
+    let formatParam = "";
     if (formatFlowed) {
       // Set format=flowed as in RFC 2646 according to the preference.
-      formatParams += "; format=flowed";
-    }
-    if (delsp) {
-      formatParams += "; delsp=yes";
+      formatParam += "; format=flowed";
     }
 
     // body is 8-bit string, save it directly in MimePart to avoid converting
     // back and forth.
     let htmlPart = null;
     let plainPart = null;
     let parts = [];
 
     if (this._bodyType === "text/html") {
       htmlPart = new MimePart(
-        charset,
         this._bodyType,
         this._compFields.forceMsgEncoding,
         true
       );
-      htmlPart.setHeader("Content-Type", `text/html${charsetParams}`);
+      htmlPart.setHeader("content-type", `text/html; charset=UTF-8`);
       htmlPart.bodyText = this._bodyText;
     } else if (this._bodyType === "text/plain") {
       plainPart = new MimePart(
-        charset,
         this._bodyType,
         this._compFields.forceMsgEncoding,
         true
       );
       plainPart.setHeader(
-        "Content-Type",
-        `text/plain${charsetParams}${formatParams}`
+        "content-type",
+        `text/plain; charset=UTF-8${formatParam}`
       );
       plainPart.bodyText = this._bodyText;
       parts.push(plainPart);
     }
 
     // Assemble a multipart/alternative message.
     if (
       (this._compFields.forcePlainText ||
         this._compFields.useMultipartAlternative) &&
       plainPart === null &&
       htmlPart !== null
     ) {
       plainPart = new MimePart(
-        charset,
         "text/plain",
         this._compFields.forceMsgEncoding,
         true
       );
       plainPart.setHeader(
-        "Content-Type",
-        `text/plain${charsetParams}${formatParams}`
+        "content-type",
+        `text/plain; charset=UTF-8${formatParam}`
       );
-      plainPart.bodyText = this._convertToPlainText(
+      plainPart.bodyText = MsgUtils.convertToPlainText(
         this._bodyText,
-        formatFlowed,
-        delsp,
-        disallowBreaks
+        formatFlowed
       );
 
       parts.push(plainPart);
     }
 
     // If useMultipartAlternative is true, send multipart/alternative message.
     // Otherwise, send the plainPart only.
     if (htmlPart) {
@@ -216,65 +325,33 @@ class MimeMessage {
     return parts;
   }
 
   /**
    * Collect local attachments.
    * @returns {Array.<MimePart>}
    */
   _gatherAttachmentParts() {
-    let charset = this._compFields.characterSet;
     let attachments = [...this._compFields.attachments];
-    let parts = [];
+    let cloudParts = [];
+    let localParts = [];
+
     for (let attachment of attachments) {
       if (attachment.sendViaCloud) {
-        // TODO: handle cloud attachments.
+        let part = new MimePart();
+        let mozillaCloudPart = MsgUtils.getXMozillaCloudPart(
+          this._deliverMode,
+          attachment
+        );
+        part.setHeader("x-mozilla-cloud-part", mozillaCloudPart);
+        part.setHeader("content-type", "application/octet-stream");
+        cloudParts.push(part);
         continue;
       }
-      let part = new MimePart(
-        charset,
-        null,
-        this._compFields.forceMsgEncoding,
-        false
-      );
+      let part = new MimePart(null, this._compFields.forceMsgEncoding, false);
       part.bodyAttachment = attachment;
-      parts.push(part);
+      localParts.push(part);
     }
-    return parts;
-  }
-
-  /**
-   * Convert html to text to form a multipart/alternative message. The output
-   * depends on preference and message charset.
-   */
-  _convertToPlainText(
-    input,
-    formatFlowed,
-    delsp,
-    formatOutput,
-    disallowBreaks
-  ) {
-    let wrapWidth = Services.prefs.getIntPref("mailnews.wraplength", 72);
-    if (wrapWidth > 990) {
-      wrapWidth = 990;
-    } else if (wrapWidth < 10) {
-      wrapWidth = 10;
-    }
-
-    let flags =
-      Ci.nsIDocumentEncoder.OutputPersistNBSP |
-      Ci.nsIDocumentEncoder.OutputFormatted;
-    if (formatFlowed) {
-      flags |= Ci.nsIDocumentEncoder.OutputFormatFlowed;
-    }
-    if (delsp) {
-      flags |= Ci.nsIDocumentEncoder.OutputFormatDelSp;
-    }
-    if (disallowBreaks) {
-      flags |= Ci.nsIDocumentEncoder.OutputDisallowLineBreaking;
-    }
-
-    let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(
-      Ci.nsIParserUtils
-    );
-    return parserUtils.convertToPlainText(input, flags, wrapWidth);
+    // 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);
   }
 }
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/src/MimeMessageUtils.jsm
@@ -0,0 +1,674 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { MailServices } = ChromeUtils.import(
+  "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+
+/**
+ * Collection of helper functions for message sending process.
+ */
+var MsgUtils = {
+  /**
+   * 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.
+   * @retuns {string}
+   */
+  convertToPlainText(input, formatFlowed) {
+    let wrapWidth = Services.prefs.getIntPref("mailnews.wraplength", 72);
+    if (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 [headerName, headerValue] = attrValue.split(":");
+        headers.push({
+          headerName,
+          headerValue,
+        });
+      }
+    }
+    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) {
+    // If the identity pref "fcc" is set to false, then we will not do
+    // any FCC operation!
+    if (!userIdentity.doFcc) {
+      return "";
+    }
+    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.trim();
+        }
+      }
+    }
+
+    // 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.ReplyWithTemplate,
+        ].includes(compType)
+      ) {
+        let msgHdr = MailServices.messenger
+          .messageServiceFromURI(originalMsgURI)
+          .messageURIToMsgHdr(originalMsgURI);
+        let folder = msgHdr.folder;
+        // let canFileMessages = folder.canFileMessages
+        let incomingServerType = folder.incomingServer.getCharValue("type");
+        if (folder.canFileMessages && incomingServerType != "rss") {
+          fcc = folder.uri;
+          useDefaultFcc = false;
+        }
+      }
+
+      if (useDefaultFcc) {
+        let uri = this.getMsgFolderURIFromPrefs(
+          userIdentity,
+          Ci.nsIMsgSend.nsMsgDeliverNow
+        );
+        fcc = uri == "nocopy://" ? "" : uri;
+      }
+    }
+
+    return fcc;
+  },
+
+  /**
+   * 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) {
+    let hasDisclosedRecipient = compFields.to || compFields.cc;
+    // 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 = `cloudFile; url=${attachment.contentLocation}`;
+    if (deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft) {
+      value += `; provider=${attachment.cloudFileAccountKey}`;
+      value += `; file=${attachment.url}`;
+    }
+    value += `; name=${attachment.name}`;
+    return value;
+  },
+
+  /**
+   * 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;
+  },
+
+  /**
+   * TODO: Pick the charset according to attachment content.
+   */
+  pickCharset(contentType, content) {
+    if (contentType.startsWith("text")) {
+      return "UTF-8";
+    }
+    return "";
+  },
+
+  /**
+   * 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 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 "";
+  },
+};
--- a/mailnews/compose/src/MimePart.jsm
+++ b/mailnews/compose/src/MimePart.jsm
@@ -1,94 +1,70 @@
 /* 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"];
 
+let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 let { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
 let { MimeEncoder } = ChromeUtils.import("resource:///modules/MimeEncoder.jsm");
+let { MsgUtils } = ChromeUtils.import(
+  "resource:///modules/MimeMessageUtils.jsm"
+);
 
 Cu.importGlobalProperties(["fetch"]);
 
 /**
- * Because ACString is 8-bit string, non-ASCII character takes multiple bytes.
- * For example, 世界 is represented as \xE4\xB8\x96\xE7\x95\x8C. This function
- * converts ACString to ArrayBuffer, which can then be passed to a TextDecoder
- * or OS.File.write.
- * @param {string} str - the string to convert to an ArrayBuffer
- * @returns {ArrayBuffer}
- */
-function byteStringToArrayBuffer(str) {
-  let strLen = str.length;
-  let buf = new ArrayBuffer(strLen);
-  let arr = new Uint8Array(buf);
-  for (let i = 0; i < strLen; i++) {
-    arr[i] = str.charCodeAt(i);
-  }
-  return buf;
-}
-
-/**
- * Convert ArrayBuffer to 8-bit string.
- * @param {ArrayBuffer} buf - the ArrayBuffer to convert to a string
- * @returns {string}
- */
-function arrayBufferToByteString(buf) {
-  let CHUNK_SIZE = 65536;
-  let arr = new Uint8Array(buf);
-  let arrLen = arr.length;
-  if (arrLen < CHUNK_SIZE) {
-    return String.fromCharCode.apply(null, arr);
-  }
-  let result = "";
-  for (let i = 0; i < Math.ceil(arrLen / CHUNK_SIZE); i++) {
-    let chunk = arr.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
-    result += String.fromCharCode.apply(null, chunk);
-  }
-  return result;
-}
-
-/**
  * 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.
  */
 class MimePart {
   /**
    * Init private properties, it's best not to access those properties directly
    * from the outside.
    */
-  constructor(
-    charset = "",
-    contentType = "",
-    forceMsgEncoding = false,
-    isMainBody = false
-  ) {
-    this._charset = charset;
+  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._separator = "";
     this._parts = [];
   }
 
   /**
    * Set a header.
-   * @param {string} name - The header name, e.g. "Content-Type"
-   * @param {string} content - The header content, e.g. "text/plain"
+   * @param {string} name - The header name, e.g. "Content-Type".
+   * @param {string} content - The header content, e.g. "text/plain".
    */
   setHeader(name, content) {
-    // _headers will be passed to jsmime, which requires header content to be an
-    // array.
-    this._headers.set(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()]);
+    }
   }
 
   /**
    * Set headers by an iterable.
    * @param {Iterable.<string, string>} entries - The header entries.
    */
   setHeaders(entries) {
     for (let [name, content] of entries) {
@@ -112,17 +88,17 @@ class MimePart {
 
   /**
    * Set the content type to multipart/<subtype>.
    * @param {string} subtype - usually "alternative" or "mixed".
    */
   initMultipart(subtype) {
     this._separator = this._makePartSeparator();
     this.setHeader(
-      "Content-Type",
+      "content-type",
       `multipart/${subtype}; boundary="${this._separator}"`
     );
   }
 
   /**
    * Add a child part.
    * @param {MimePart} part - A MimePart.
    */
@@ -141,37 +117,51 @@ class MimePart {
   /**
    * Fetch the attachment file to get its content type and content.
    * @returns {string}
    */
   async fetchFile() {
     let res = await fetch(this._bodyAttachment.url);
     this._contentType = res.headers.get("content-type");
 
-    // File name can contain non-ASCII chars, encode according to RFC 2047.
-    let encodedName = this._encodeHeaderParameter(
+    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 = MsgUtils.rfc2231ParamFolding(
       "name",
       this._bodyAttachment.name
     );
-    let encodedFileName = this._encodeHeaderParameter(
+    let encodedFileName = MsgUtils.rfc2231ParamFolding(
       "filename",
       this._bodyAttachment.name
     );
-    this.setHeader(
-      "Content-Type",
-      `${this._contentType}; name="${encodedName}"`
-    );
-    this.setHeader(
-      "Content-Disposition",
-      `attachment; filename="${encodedFileName}"`
-    );
+
+    let buf = await res.arrayBuffer();
+    let content = jsmime.mimeutils.typedArrayToString(new Uint8Array(buf));
+    this._charset = MsgUtils.pickCharset(this._contentType, content);
 
-    // Determine Content-Transfer-Encoding and encode file content accordingly.
-    let buf = await res.arrayBuffer();
-    return arrayBufferToByteString(buf);
+    let contentTypeParams = "";
+    if (this._charset) {
+      contentTypeParams += `; charset=${this._charset}`;
+    }
+    if (parmFolding != 2) {
+      contentTypeParams += `; "${encodedName}"`;
+    }
+    this.setHeader("content-type", `${this._contentType}${contentTypeParams}`);
+    this.setHeader("content-disposition", `attachment; ${encodedFileName}`);
+    if (this._contentType == "text/html") {
+      let contentLocation = MsgUtils.getContentLocation(
+        this._bodyAttachment.url
+      );
+      this.setHeader("content-location", contentLocation);
+    }
+
+    return content;
   }
 
   /**
    * Recursively write a MimePart and its parts to a file.
    * @param {OS.File} file - The output file to contain a RFC2045 message.
    */
   async write(file) {
     this._outFile = file;
@@ -184,23 +174,27 @@ class MimePart {
       let encoder = new MimeEncoder(
         this._charset,
         this._contentType,
         this._forceMsgEncoding,
         this._isMainBody,
         bodyString
       );
       encoder.pickEncoding();
-      this.setHeader("Content-Transfer-Encoding", encoder.encoding);
+      this.setHeader("content-transfer-encoding", encoder.encoding);
       bodyString = encoder.encode();
+    } else if (this._isMainBody) {
+      this.setHeader("content-transfer-encoding", "7bit");
     }
 
     // Write out headers.
     await this._writeString(
-      jsmime.headeremitter.emitStructuredHeaders(this._headers, {})
+      jsmime.headeremitter.emitStructuredHeaders(this._headers, {
+        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._writeString(`${bodyString}\r\n`);
@@ -217,46 +211,29 @@ class MimePart {
       await this._writeString(`--${this._separator}--\r\n`);
     }
 
     // Write out body.
     await this._writeString(`\r\n${bodyString}\r\n`);
   }
 
   /**
-   * Use TextEncoder.encode would be incorrect here since the argument is not
-   * UTF-8 string.
+   * Write a string to this._outFile.
+   * @param {string} str - The string to write.
    */
   async _writeString(str) {
-    await this._outFile.write(new DataView(byteStringToArrayBuffer(str)));
+    await this._outFile.write(new TextEncoder().encode(str));
   }
 
   /**
    * Use 12 hyphen characters and 24 random base64 characters as separator.
    */
   _makePartSeparator() {
     return (
       "------------" +
       btoa(
         String.fromCharCode(
           ...[...Array(18)].map(() => Math.floor(Math.random() * 256))
         )
       )
     );
   }
-
-  /**
-   * Use nsIMimeConverter to encode header parameter according to RFC 2047.
-   * @param {string} name - The parameter name, e.g. "filename"
-   * @param {string} value - The parameter value, e.g. "screen.png"
-   */
-  _encodeHeaderParameter(name, value) {
-    let converter = Cc["@mozilla.org/messenger/mimeconverter;1"].getService(
-      Ci.nsIMimeConverter
-    );
-    return converter.encodeMimePartIIStr_UTF8(
-      value,
-      false,
-      name.length,
-      Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE
-    );
-  }
 }
--- a/mailnews/compose/src/components.conf
+++ b/mailnews/compose/src/components.conf
@@ -1,16 +1,22 @@
 # -*- 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': '{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': '{5de59b50-22d5-4e77-ae9f-9c336d339798}',
     'contract_ids': ['@mozilla.org/messengercompose/send-module-loader;1'],
--- a/mailnews/compose/src/moz.build
+++ b/mailnews/compose/src/moz.build
@@ -57,16 +57,17 @@ FINAL_LIBRARY = 'mail'
 # clang-cl rightly complains about switch on nsresult.
 if CONFIG['CC_TYPE'] == 'clang-cl':
     CXXFLAGS += ['-Wno-switch']
 
 EXTRA_JS_MODULES += [
     'MessageSend.jsm',
     'MimeEncoder.jsm',
     'MimeMessage.jsm',
+    'MimeMessageUtils.jsm',
     'MimePart.jsm',
     'MsgSendModuleLoader.jsm',
     'SMTPProtocolHandler.jsm',
 ]
 
 XPCOM_MANIFESTS += [
     'components.conf',
 ]
--- a/mailnews/compose/src/nsMsgCompUtils.cpp
+++ b/mailnews/compose/src/nsMsgCompUtils.cpp
@@ -702,19 +702,19 @@ char* mime_generate_attachment_headers(
 
     // rhp - Put in a pref for using Content-Location instead of Content-Base.
     //       This will get tweaked to default to true in 5.0
     if (prefs)
       prefs->GetBoolPref("mail.use_content_location_on_send",
                          &useContentLocation);
 
     if (useContentLocation)
-      buf.AppendLiteral("Content-Location: \"");
+      buf.AppendLiteral("Content-Location: ");
     else
-      buf.AppendLiteral("Content-Base: \"");
+      buf.AppendLiteral("Content-Base: ");
     /* rhp - Pref for Content-Location usage */
 
   /* rhp: this is to work with the Content-Location stuff */
   CONTENT_LOC_HACK:
 
     while (*s != 0 && *s != '#') {
       uint32_t ot = buf.Length();
       char tmp[] = "\x00\x00";
@@ -734,21 +734,21 @@ char* mime_generate_attachment_headers(
         buf.AppendLiteral("%0D");
       else {
         tmp[0] = *s;
         buf.Append(tmp);
       }
       s++;
       col += (buf.Length() - ot);
     }
-    buf.AppendLiteral("\"" CRLF);
+    buf.AppendLiteral(CRLF);
 
     // rhp: this is to try to get around this fun problem with Content-Location
     if (!useContentLocation) {
-      buf.AppendLiteral("Content-Location: \"");
+      buf.AppendLiteral("Content-Location: ");
       s = base_url;
       col = 0;
       useContentLocation = true;
       goto CONTENT_LOC_HACK;
     }
     // rhp: this is to try to get around this fun problem with Content-Location
 
   GIVE_UP_ON_CONTENT_BASE:;
--- a/mailnews/compose/src/nsMsgCopy.cpp
+++ b/mailnews/compose/src/nsMsgCopy.cpp
@@ -100,40 +100,40 @@ nsresult CopyListener::SetMsgComposeAndS
 }
 
 ////////////////////////////////////////////////////////////////////////////////////
 // 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, nsIUrlListener)
+NS_IMPL_ISUPPORTS(nsMsgCopy, nsIMsgCopy, nsIUrlListener)
 
 nsMsgCopy::nsMsgCopy() {
   mFile = nullptr;
   mMode = nsIMsgSend::nsMsgDeliverNow;
   mSavePref = nullptr;
 }
 
 nsMsgCopy::~nsMsgCopy() { PR_Free(mSavePref); }
 
-nsresult nsMsgCopy::StartCopyOperation(nsIMsgIdentity* aUserIdentity,
-                                       nsIFile* aFile, nsMsgDeliverMode aMode,
-                                       nsIMsgSend* aMsgSendObj,
-                                       const char* aSavePref,
-                                       nsIMsgDBHdr* aMsgToReplace) {
+NS_IMETHODIMP
+nsMsgCopy::StartCopyOperation(nsIMsgIdentity* aUserIdentity, nsIFile* aFile,
+                              nsMsgDeliverMode aMode, nsIMsgSend* aMsgSendObj,
+                              const nsACString& aSavePref,
+                              nsIMsgDBHdr* aMsgToReplace) {
   nsCOMPtr<nsIMsgFolder> dstFolder;
   bool isDraft = false;
   bool waitForUrl = false;
   nsresult rv;
 
   if (!aMsgSendObj) return NS_ERROR_INVALID_ARG;
 
   // Store away the server location...
-  if (aSavePref) mSavePref = PL_strdup(aSavePref);
+  if (!aSavePref.IsEmpty()) mSavePref = ToNewCString(aSavePref);
 
   //
   // Vars for implementation...
   //
 
   // QueueForLater (Outbox)
   if (aMode == nsIMsgSend::nsMsgQueueForLater ||
       aMode == nsIMsgSend::nsMsgDeliverBackground) {
--- a/mailnews/compose/src/nsMsgCopy.h
+++ b/mailnews/compose/src/nsMsgCopy.h
@@ -7,23 +7,20 @@
 #define _nsMsgCopy_H_
 
 #include "mozilla/Attributes.h"
 #include "nscore.h"
 #include "nsIFile.h"
 #include "nsMsgSend.h"
 #include "nsIMsgFolder.h"
 #include "nsITransactionManager.h"
+#include "nsIMsgCopy.h"
 #include "nsIMsgCopyServiceListener.h"
 #include "nsIMsgCopyService.h"
 
-// {0874C3B5-317D-11d3-8EFB-00A024A7D144}
-#define NS_IMSGCOPY_IID \
-  {0x874c3b5, 0x317d, 0x11d3, {0x8e, 0xfb, 0x0, 0xa0, 0x24, 0xa7, 0xd1, 0x44}};
-
 // 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 {
@@ -51,33 +48,29 @@ class CopyListener : public nsIMsgCopySe
   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 nsIUrlListener {
+class nsMsgCopy : public nsIMsgCopy, public nsIUrlListener {
  public:
   nsMsgCopy();
 
   // nsISupports interface
   NS_DECL_ISUPPORTS
+  NS_DECL_NSIMSGCOPY
   NS_DECL_NSIURLLISTENER
 
   //////////////////////////////////////////////////////////////////////
   // Object methods...
   //////////////////////////////////////////////////////////////////////
   //
-  nsresult StartCopyOperation(nsIMsgIdentity* aUserIdentity, nsIFile* aFile,
-                              nsMsgDeliverMode aMode, nsIMsgSend* aMsgSendObj,
-                              const char* aSavePref,
-                              nsIMsgDBHdr* aMsgToReplace);
-
   nsresult DoCopy(nsIFile* aDiskFile, nsIMsgFolder* dstFolder,
                   nsIMsgDBHdr* aMsgToReplace, bool aIsDraft,
                   nsIMsgWindow* msgWindow, nsIMsgSend* aMsgSendObj);
 
   nsresult GetUnsentMessagesFolder(nsIMsgIdentity* userIdentity,
                                    nsIMsgFolder** msgFolder, bool* waitForUrl);
   nsresult GetDraftsFolder(nsIMsgIdentity* userIdentity,
                            nsIMsgFolder** msgFolder, bool* waitForUrl);
--- a/mailnews/compose/src/nsMsgSend.cpp
+++ b/mailnews/compose/src/nsMsgSend.cpp
@@ -4240,17 +4240,17 @@ nsresult nsMsgComposeAndSend::StartMessa
   if (!dest_uri.IsEmpty())
     m_folderName = dest_uri;
   else
     GetFolderURIFromUserPrefs(mode, mUserIdentity, m_folderName);
 
   if (mListener) mListener->OnGetDraftFolderURI(m_folderName.get());
 
   rv = mCopyObj->StartCopyOperation(mUserIdentity, aFile, mode, this,
-                                    m_folderName.get(), mMsgToReplace);
+                                    m_folderName, mMsgToReplace);
   return rv;
 }
 
 // I'm getting this each time without holding onto the feedback so that 3 pane
 // windows can be closed without any chance of crashing due to holding onto a
 // deleted feedback.
 nsresult nsMsgComposeAndSend::SetStatusMessage(const nsString& aMsgString) {
   if (mSendProgress)
--- a/mailnews/compose/test/moz.build
+++ b/mailnews/compose/test/moz.build
@@ -1,7 +1,9 @@
 # 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']
-
+XPCSHELL_TESTS_MANIFESTS += [
+    'unit/xpcshell-cpp.ini',
+    'unit/xpcshell-jsm.ini',
+]
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/test/unit/head_compose_jsm.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Set mailnews.send.jsmodule to true, so that test suite will be run against
+ * MessageSend.jsm.
+ */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+Services.prefs.setBoolPref("mailnews.send.jsmodule", true);
+
+// Trigger the loading of MessageSend.jsm.
+Cc["@mozilla.org/messengercompose/send-module-loader;1"].getService();
--- a/mailnews/compose/test/unit/test_messageHeaders.js
+++ b/mailnews/compose/test/unit/test_messageHeaders.js
@@ -76,20 +76,27 @@ function checkMessageHeaders(msgData, ex
     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));
+          Assert.ok(
+            !headers.has(header),
+            `Should not have header named "${header}"`
+          );
         } else {
           let value = headers.getRawHeader(header);
-          Assert.equal(value.length, 1);
+          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) {
@@ -346,28 +353,28 @@ async function testSendHeaders() {
     "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",
-    "X-Custom-2": "=?UTF-8?Q?_Enchant=c3=a9?=",
+    "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",
-    "X-Custom-2": "=?UTF-8?Q?_Enchant=c3=a9?=",
+    "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);
@@ -426,23 +433,21 @@ async function testContentHeaders() {
 
   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-Base": '"data:text/html,<html></html>"',
-    "Content-Location": '"data:text/html,<html></html>"',
+    "Content-Location": "data:text/html,<html></html>",
   };
   await richCreateMessage(fields, [httpAttachment], identity);
   checkDraftHeaders(
     {
-      "Content-Base": undefined,
       "Content-Location": undefined,
     },
     "1"
   );
   checkDraftHeaders(httpAttachmentHeaders, "2");
 
   let cloudAttachment = makeAttachment({
     url: Services.io.newFileURI(do_get_file("data/test-UTF-8.txt")).spec,
@@ -573,53 +578,60 @@ async function testSentMessage() {
         cc: "Alex <alex@tinderbox.invalid>",
         bcc: "Boris <boris@tinderbox.invalid>",
         replyTo: "Charles <charles@tinderbox.invalid>",
       },
       identity,
       {},
       []
     );
+    server.performTest();
     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);
+    server.performTest();
     checkMessageHeaders(daemon.post, {
       To: "undisclosed-recipients: ;",
     });
+    server.resetTest();
     await sendMessage(
       {
         to: "Somebody <test@tinderbox.invalid>",
         returnReceipt: true,
         receiptHeaderType: Ci.nsIMsgMdnGenerator.eDntRrtType,
       },
       identity
     );
+    server.performTest();
     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,
       cloudFileAccountKey: "akey",
       name: "attachment.html",
       contentLocation: "http://localhost.invalid/",
     });
     await sendMessage({ to: "test@tinderbox.invalid" }, identity, {}, [
       cloudAttachment,
     ]);
+    server.performTest();
     checkMessageHeaders(
       daemon.post,
       {
         "Content-Type": "application/octet-stream",
         "X-Mozilla-Cloud-Part":
           "cloudFile; url=http://localhost.invalid/; name=attachment.html",
       },
       "2"
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/test/unit/xpcshell-cpp.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+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]
+[test_smtp8bitMime.js]
+[test_smtpAuthMethods.js]
+[test_smtpPassword.js]
+[test_smtpPassword2.js]
+[test_smtpPasswordFailure1.js]
+[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
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/test/unit/xpcshell-jsm.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head = head_compose_jsm.js head_compose.js
+tail =
+dupe-manifest =
+support-files = data/*
+
+[include:xpcshell-shared.ini]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/compose/test/unit/xpcshell-shared.ini
@@ -0,0 +1,1 @@
+[test_messageHeaders.js]
\ No newline at end of file
deleted file mode 100644
--- a/mailnews/compose/test/unit/xpcshell.ini
+++ /dev/null
@@ -1,47 +0,0 @@
-[DEFAULT]
-head = head_compose.js
-tail =
-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_messageHeaders.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]
-[test_smtp8bitMime.js]
-[test_smtpAuthMethods.js]
-[test_smtpPassword.js]
-[test_smtpPassword2.js]
-[test_smtpPasswordFailure1.js]
-[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]
--- a/mailnews/mime/jsmime/jsmime.js
+++ b/mailnews/mime/jsmime/jsmime.js
@@ -92,18 +92,18 @@
         typedarray[i] = buffer.charCodeAt(i);
       }
       return typedarray;
     }
 
     /**
      * Converts a Uint8Array buffer to a binary string.
      *
-     * @param buffer {BinaryString} The string to convert.
-     * @returns {Uint8Array} The converted data.
+     * @param buffer {Uint8Array} The Uint8Array to convert.
+     * @returns {string} The converted string.
      */
     function typedArrayToString(buffer) {
       var string = "";
       for (let i = 0; i < buffer.length; i += 100) {
         string += String.fromCharCode.apply(
           undefined,
           buffer.subarray(i, i + 100)
         );
@@ -3643,15 +3643,16 @@
       emitStructuredHeader,
       emitStructuredHeaders,
       makeStreamingEmitter,
     });
   });
 
   def("jsmime", function(require) {
     return {
+      mimeutils: require("./mimeutils"),
       MimeParser: require("./mimeparser"),
       headerparser: require("./headerparser"),
       headeremitter: require("./headeremitter"),
     };
   });
   return mods.jsmime;
 });
--- a/mailnews/mime/src/MimeJSComponents.jsm
+++ b/mailnews/mime/src/MimeJSComponents.jsm
@@ -214,17 +214,17 @@ MimeWritableStructuredHeaders.prototype 
     try {
       this.setHeader(
         aHeaderName,
         jsmime.headerparser.parseStructuredHeader(aHeaderName, aValue)
       );
     } catch (e) {
       // This means we don't have a structured encoder. Just assume it's a raw
       // string value then.
-      this.setHeader(aHeaderName, aValue);
+      this.setHeader(aHeaderName, aValue.trim());
     }
   },
 };
 
 // These are prototypes for nsIMsgHeaderParser implementation
 var Mailbox = {
   toString() {
     return this.name ? this.name + " <" + this.email + ">" : this.email;