Bug 1005413: Import JSMime from revision 8626a9039d66570124a9d1d7e54683d50f0f6570, r=irving.
authorJoshua Cranmer <Pidgeot18@gmail.com>
Mon, 13 Oct 2014 18:43:49 -0500
changeset 21098 3f8c1a00b76ab1569f931b57a9a957d32ef6b405
parent 21097 35295a033274825a5a12f60b363ffdfa06d3a7e1
child 21099 b1570c0200ffd2826033a1cccbf936d60612bb87
push id1274
push usermbanner@mozilla.com
push dateMon, 12 Jan 2015 19:54:49 +0000
treeherdercomm-beta@baea280adc1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersirving
bugs1005413
Bug 1005413: Import JSMime from revision 8626a9039d66570124a9d1d7e54683d50f0f6570, r=irving. Individual JSMime patches were reviewed by both irving and asuth.
mailnews/mime/jsmime/jsmime.js
mailnews/mime/jsmime/test/head_xpcshell_glue.js
mailnews/mime/jsmime/test/mock_date.js
mailnews/mime/jsmime/test/test_header.js
mailnews/mime/jsmime/test/test_header_emitter.js
mailnews/mime/jsmime/test/test_structured_header_emitters.js
mailnews/mime/jsmime/test/test_structured_headers.js
mailnews/mime/moz.build
--- a/mailnews/mime/jsmime/jsmime.js
+++ b/mailnews/mime/jsmime/jsmime.js
@@ -91,19 +91,24 @@ function stringToTypedArray(buffer) {
  */
 function typedArrayToString(buffer) {
   var string = '';
   for (var i = 0; i < buffer.length; i+= 100)
     string += String.fromCharCode.apply(undefined, buffer.subarray(i, i + 100));
   return string;
 }
 
+/** A list of month names for Date parsing. */
+const kMonthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
+  "Sep", "Oct", "Nov", "Dec"];
+
 return {
   decode_base64: decode_base64,
   decode_qp: decode_qp,
+  kMonthNames: kMonthNames,
   stringToTypedArray: stringToTypedArray,
   typedArrayToString: typedArrayToString,
 };
 });
 /**
  * This file implements knowledge of how to encode or decode structured headers
  * for several key headers. It is not meant to be used externally to jsmime.
  */
@@ -200,21 +205,32 @@ function parseUnstructured(values) {
 function writeUnstructured(value) {
   this.addUnstructured(value);
 }
 
 // RFC 5322
 addHeader("Comments", parseUnstructured, writeUnstructured);
 addHeader("Keywords", parseUnstructured, writeUnstructured);
 addHeader("Subject", parseUnstructured, writeUnstructured);
-
 // RFC 2045
 addHeader("Content-Description", parseUnstructured, writeUnstructured);
 
 
+// Date headers
+function parseDate(values) { return this.parseDateHeader(values[0]); }
+function writeDate(value) { this.addDate(value); }
+
+// RFC 5322
+addHeader("Date", parseDate, writeDate);
+addHeader("Resent-Date", parseDate, writeDate);
+// RFC 5536
+addHeader("Expires", parseDate, writeDate);
+addHeader("Injection-Date", parseDate, writeDate);
+addHeader("NNTP-Posting-Date", parseDate, writeDate);
+
 
 // Miscellaneous headers (those that don't fall under the above schemes):
 
 // RFC 2047
 structuredDecoders.set("Content-Transfer-Encoding", function (values) {
   return values[0].toLowerCase();
 });
 structuredEncoders.set("Content-Transfer-Encoding", writeUnstructured);
@@ -305,21 +321,26 @@ var headerparser = {};
  * @param {String} delimiters A set of delimiters to include as individual
  *                            tokens.
  * @param {Object} opts       A set of options selecting what to parse.
  * @param {Boolean} [opts.qstring]  If true, recognize quoted strings.
  * @param {Boolean} [opts.dliteral] If true, recognize domain literals.
  * @param {Boolean} [opts.comments] If true, recognize comments.
  * @param {Boolean} [opts.rfc2047]  If true, parse and decode RFC 2047
  *                                  encoded-words.
- * @returns {(Token|String)*} A sequence of Token objects (which have a
- *                            toString method returning their value) or String
- *                            objects (representing delimiters).
+ * @returns {(Token|String)[]} An array of Token objects (which have a toString
+ *                             method returning their value) or String objects
+ *                             (representing delimiters).
  */
-function* getHeaderTokens(value, delimiters, opts) {
+function getHeaderTokens(value, delimiters, opts) {
+  // The array of parsed tokens. This method used to be a generator, but it
+  // appears that generators are poorly optimized in current engines, so it was
+  // converted to not be one.
+  let tokenList = [];
+
   /// Represents a non-delimiter token
   function Token(token) {
     // Unescape all quoted pairs. Any trailing \ is deleted.
     this.token = token.replace(/\\(.?)/g, "$1");
   }
   Token.prototype.toString = function () { return this.token; };
 
   // The start of the current token (e.g., atoms, strings)
@@ -353,22 +374,22 @@ function* getHeaderTokens(value, delimit
 
         // If RFC 2047 is enabled, decode the qstring only if the entire string
         // appears to be a 2047 token. Don't unquote just yet (this will better
         // match people who incorrectly treat RFC 2047 decoding as a separate,
         // earlier step).
         if (opts.rfc2047 && text.startsWith("=?") && text.endsWith("?="))
           text = decodeRFC2047Words(text);
 
-        yield new Token(text);
+        tokenList.push(new Token(text));
         endQuote = undefined;
         tokenStart = undefined;
       } else if (ch == endQuote && ch == ']') {
         // Domain literals include their delimiters.
-        yield new Token(value.slice(tokenStart, i + 1));
+        tokenList.push(new Token(value.slice(tokenStart, i + 1)));
         endQuote = undefined;
         tokenStart = undefined;
       }
       // Avoid any further processing.
       continue;
     }
 
     // If we can match the RFC 2047 encoded-word pattern, we need to decode the
@@ -377,27 +398,27 @@ function* getHeaderTokens(value, delimit
       // RFC 2047 tokens separated only by whitespace are conceptually part of
       // the same output token, so we need to decode them all at once.
       let encodedWordsRE = /([ \t\r\n]*=\?[^?]*\?[BbQq]\?[^?]*\?=)+/;
       let result = encodedWordsRE.exec(value.slice(i));
       if (result !== null) {
         // If we were in the middle of a prior token (i.e., something like
         // foobar=?UTF-8?Q?blah?=), yield the previous segment as a token.
         if (tokenStart !== undefined) {
-          yield new Token(value.slice(tokenStart, i));
+          tokenList.push(new Token(value.slice(tokenStart, i)));
           tokenStart = undefined;
         }
 
         // Find out how much we need to decode...
         let encWordsLen = result[0].length;
         let string = decodeRFC2047Words(value.slice(i, i + encWordsLen),
           "UTF-8");
         // Don't make a new Token variable, since we do not want to unescape the
         // decoded string.
-        yield { toString: function() { return string; }};
+        tokenList.push({ toString: function() { return string; }});
 
         // Skip everything we decoded. The -1 is because we don't want to
         // include the starting character.
         i += encWordsLen - 1;
         continue;
       }
 
       // If we are here, then we failed to match the simple 2047 encoded-word
@@ -455,39 +476,41 @@ function* getHeaderTokens(value, delimit
       // Not a delimiter, whitespace, comment, domain literal, or quoted string.
       // Must be part of an atom then!
       tokenIsStarting = true;
     }
 
     // If our analysis concluded that we closed an open token, and there is an
     // open token, then yield that token.
     if (tokenIsEnding && tokenStart !== undefined) {
-      yield new Token(value.slice(tokenStart, i));
+      tokenList.push(new Token(value.slice(tokenStart, i)));
       tokenStart = undefined;
     }
     // If we need to output a delimiter, do so.
     if (isSpecial)
-      yield ch;
+      tokenList.push(ch);
     // If our analysis concluded that we could open a token, and no token is
     // opened yet, then start the token.
     if (tokenIsStarting && tokenStart === undefined) {
       tokenStart = i;
     }
   }
 
   // That concludes the loop! If there is a currently open token, close that
   // token now.
   if (tokenStart !== undefined) {
     // Error case: a partially-open quoted string is assumed to have a trailing
     // " character.
     if (endQuote == '"')
-      yield new Token(value.slice(tokenStart + 1));
+      tokenList.push(new Token(value.slice(tokenStart + 1)));
     else
-      yield new Token(value.slice(tokenStart));
+      tokenList.push(new Token(value.slice(tokenStart)));
   }
+
+  return tokenList;
 }
 
 /**
  * Convert a header value into UTF-16 strings by attempting to decode as UTF-8
  * or another legacy charset. If the header is valid UTF-8, it will be decoded
  * as UTF-8; if it is not, the fallbackCharset will be attempted instead.
  *
  * @param {String} headerValue       The header (as a binary string) to attempt
@@ -1010,16 +1033,123 @@ function decode2231Value(value) {
   let typedarray = mimeutils.stringToTypedArray(value);
 
   // Decode the charset. If the charset isn't found, we throw an error. Try to
   // fallback in that case.
   return new TextDecoder(charset, {fatal: true})
     .decode(typedarray, {stream: false});
 }
 
+// This is a map of known timezone abbreviations, for fallback in obsolete Date
+// productions.
+const kKnownTZs = {
+  // The following timezones are explicitly listed in RFC 5322.
+  "UT":  "+0000", "GMT": "+0000",
+  "EST": "-0500", "EDT": "-0400",
+  "CST": "-0600", "CDT": "-0500",
+  "MST": "-0700", "MDT": "-0600",
+  "PST": "-0800", "PDT": "-0700",
+  // The following are time zones copied from NSPR's prtime.c
+  "AST": "-0400", // Atlantic Standard Time
+  "NST": "-0330", // Newfoundland Standard Time
+  "BST": "+0100", // British Summer Time
+  "MET": "+0100", // Middle Europe Time
+  "EET": "+0200", // Eastern Europe Time
+  "JST": "+0900"  // Japan Standard Time
+};
+
+/**
+ * Parse a header that contains a date-time definition according to RFC 5322.
+ * The result is a JS date object with the same timestamp as the header.
+ *
+ * The dates returned by this parser cannot be reliably converted back into the
+ * original header for two reasons. First, JS date objects cannot retain the
+ * timezone information they were initialized with, so reserializing a date
+ * header would necessarily produce a date in either the current timezone or in
+ * UTC. Second, JS dates measure time as seconds elapsed from the POSIX epoch
+ * excluding leap seconds. Any timestamp containing a leap second is instead
+ * converted into one that represents the next second.
+ *
+ * Dates that do not match the RFC 5322 production are instead attempted to
+ * parse using the Date.parse function. The strings that are accepted by
+ * Date.parse are not fully defined by the standard, but most implementations
+ * should accept strings that look rather close to RFC 5322 strings. Truly
+ * invalid dates produce a formulation that results in an invalid date,
+ * detectable by having its .getTime() method return NaN.
+ *
+ * @param {String} header The MIME header value to parse.
+ * @returns {Date}        The date contained within the header, as described
+ *                        above.
+ */
+function parseDateHeader(header) {
+  let tokens = [for (x of getHeaderTokens(header, ",:", {})) x.toString()];
+  // What does a Date header look like? In practice, most date headers devolve
+  // into Date: [dow ,] dom mon year hh:mm:ss tzoff [(abbrev)], with the day of
+  // week mostly present and the timezone abbreviation mostly absent.
+
+  // First, ignore the day-of-the-week if present. This would be the first two
+  // tokens.
+  if (tokens.length > 1 && tokens[1] === ',')
+    tokens = tokens.slice(2);
+
+  // If there are too few tokens, the date is obviously invalid.
+  if (tokens.length < 8)
+    return new Date(NaN);
+
+  // Save off the numeric tokens
+  let day = parseInt(tokens[0]);
+  // month is tokens[1]
+  let year = parseInt(tokens[2]);
+  let hours = parseInt(tokens[3]);
+  // tokens[4] === ':'
+  let minutes = parseInt(tokens[5]);
+  // tokens[6] === ':'
+  let seconds = parseInt(tokens[7]);
+
+  // Compute the month. Check only the first three digits for equality; this
+  // allows us to accept, e.g., "January" in lieu of "Jan."
+  let month = mimeutils.kMonthNames.indexOf(tokens[1].slice(0, 3));
+  // If the month name is not recognized, make the result illegal.
+  if (month < 0)
+    month = NaN;
+
+  // Compute the full year if it's only 2 digits. RFC 5322 states that the
+  // cutoff is 50 instead of 70.
+  if (year < 100) {
+    year += year < 50 ? 2000 : 1900;
+  }
+
+  // Compute the timezone offset. If it's not in the form ±hhmm, convert it to
+  // that form.
+  let tzoffset = tokens[8];
+  if (tzoffset in kKnownTZs)
+    tzoffset = kKnownTZs[tzoffset];
+  let decompose = /^([+-])(\d\d)(\d\d)$/.exec(tzoffset);
+  // Unknown? Make it +0000
+  if (decompose === null)
+    decompose = ['+0000', '+', '00', '00'];
+  let tzOffsetInMin = parseInt(decompose[2]) * 60 + parseInt(decompose[3]);
+  if (decompose[1] == '-')
+    tzOffsetInMin = -tzOffsetInMin;
+
+  // How do we make the date at this point? Well, the JS date's constructor
+  // builds the time in terms of the local timezone. To account for the offset
+  // properly, we need to build in UTC.
+  let finalDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds)
+    - tzOffsetInMin * 60 * 1000);
+
+  // Suppose our header was mangled and we couldn't read it--some of the fields
+  // became undefined. In that case, the date would become invalid, and the
+  // indication that it is so is that the underlying number is a NaN. In that
+  // scenario, we could build attempt to use JS Date parsing as a last-ditch
+  // attempt. But it's not clear that such messages really exist in practice,
+  // and the valid formats for Date in ES6 are unspecified.
+  return finalDate;
+}
+
 ////////////////////////////////////////
 // Structured header decoding support //
 ////////////////////////////////////////
 
 // Load the default structured decoders
 var structuredDecoders = new Map();
 var structuredHeaders = require('./structuredHeaders');
 var preferredSpellings = structuredHeaders.spellings;
@@ -1059,17 +1189,21 @@ for (let pair of structuredHeaders.decod
  * - Resent-From
  * - Resent-Sender
  * - Resent-To
  * - Return-Receipt-To
  * - Sender
  * - To
  *
  * Date headers (results are the same as parseDateHeader):
- * - (TODO: Parsing support for these headers is currently unsupported)
+ * - Date
+ * - Expires
+ * - Injection-Date
+ * - NNTP-Posting-Date
+ * - Resent-Date
  *
  * References headers (results are the same as parseReferencesHeader):
  * - (TODO: Parsing support for these headers is currently unsupported)
  *
  * Message-ID headers (results are the first entry of the result of
  * parseReferencesHeader):
  * - (TODO: Parsing support for these headers is currently unsupported)
  *
@@ -1149,16 +1283,17 @@ function addStructuredDecoder(header, de
     preferredSpellings.set(lowerHeader, header);
 }
 
 headerparser.addStructuredDecoder = addStructuredDecoder;
 headerparser.convert8BitHeader = convert8BitHeader;
 headerparser.decodeRFC2047Words = decodeRFC2047Words;
 headerparser.getHeaderTokens = getHeaderTokens;
 headerparser.parseAddressingHeader = parseAddressingHeader;
+headerparser.parseDateHeader = parseDateHeader;
 headerparser.parseParameterHeader = parseParameterHeader;
 headerparser.parseStructuredHeader = parseStructuredHeader;
 return Object.freeze(headerparser);
 
 });
 
 ////////////////////////////////////////////////////////////////////////////////
 //                        JavaScript Raw MIME Parser                          //
@@ -2230,17 +2365,17 @@ def('headeremitter', function(require) {
  * same forms that would be parsed.
  */
 
 "use strict";
 
 var mimeutils = require('./mimeutils');
 
 // Get the default structured encoders and add them to the map
-var structuredHeaders = require('structuredHeaders');
+var structuredHeaders = require('./structuredHeaders');
 var encoders = new Map();
 var preferredSpellings = structuredHeaders.spellings;
 for (let [header, encoder] of structuredHeaders.encoders) {
   addStructuredEncoder(header, encoder);
 }
 
 /// Clamp a value in the range [min, max], defaulting to def if it is undefined.
 function clamp(value, min, max, def) {
@@ -2493,21 +2628,23 @@ HeaderEmitter.prototype.addText = functi
  * @protected
  * @param {String}  text          The text to add to the output.
  * @param {String}  qchars        The set of characters that cannot appear
  *                                outside of a quoted string.
  * @param {Boolean} mayBreakAfter If true, the end of this text is a preferred
  *                                breakpoint.
  */
 HeaderEmitter.prototype.addQuotable = function (text, qchars, mayBreakAfter) {
+  // No text -> no need to be quoted (prevents strict warning errors).
+  if (text.length == 0)
+    return;
+
   // Figure out if we need to quote the string. Don't quote a string which
   // already appears to be quoted.
   let needsQuote = false;
-  if (!text.length)
-    return;
 
   if (!(text[0] == '"' && text[text.length - 1] == '"') && qchars != '') {
     for (let i = 0; i < text.length; i++) {
       if (qchars.contains(text[i])) {
         needsQuote = true;
         break;
       }
     }
@@ -2740,16 +2877,23 @@ HeaderEmitter.prototype.addStructuredHea
  * @see headerparser.parseAddressingHeader
  */
 HeaderEmitter.prototype.addAddress = function (addr) {
   // If we have a display name, add that first.
   if (addr.name) {
     // This is a simple estimate that keeps names on one line if possible.
     this._reserveTokenSpace(addr.name.length + addr.email.length + 3);
     this.addPhrase(addr.name, ",()<>:;.\"", true);
+
+    // If we don't have an email address, don't write out the angle brackets for
+    // the address. It's already an abnormal situation should this appear, and
+    // this has better round-tripping properties.
+    if (!addr.email)
+      return;
+
     this.addText("<", false);
   }
 
   // Find the local-part and domain of the address, since the local-part may
   // need to be quoted separately. Note that the @ goes to the domain, so that
   // the local-part may be quoted if it needs to be.
   let at = addr.email.lastIndexOf("@");
   let localpart = "", domain = ""
@@ -2777,20 +2921,16 @@ HeaderEmitter.prototype.addAddress = fun
  * @param {String}    [addrs[i].email] The email of the address to add.
  * @param {Address[]} [addrs[i].group] A list of email addresses in the group.
  * @see HeaderEmitter.addAddress
  * @see headerparser.parseAddressingHeader
  */
 HeaderEmitter.prototype.addAddresses = function (addresses) {
   let needsComma = false;
   for (let addr of addresses) {
-    // Ignore a dummy empty address.
-    if ("email" in addr && addr.email === "")
-      continue;
-
     // Add a comma if this is not the first element.
     if (needsComma)
       this.addText(", ", true);
     needsComma = true;
 
     if ("email" in addr) {
       this.addAddress(addr);
     } else {
@@ -2817,16 +2957,75 @@ HeaderEmitter.prototype.addUnstructured 
   if (text.length == 0)
     return;
 
   // Unstructured text is basically a phrase that can't be quoted. So, if we
   // have nothing in qchars, nothing should be quoted.
   this.addPhrase(text, "", false);
 };
 
+/** RFC 822 labels for days of the week. */
+const kDaysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+/**
+ * Formatting helper to output numbers between 0-9 as 00-09 instead.
+ */
+function padTo2Digits(num) {
+  return num < 10 ? "0" + num : num.toString();
+}
+
+/**
+ * Add a date/time field to the output, using the JS date object as the time
+ * representation. The value will be output using the timezone offset of the
+ * date object, which is usually the timezone of the user (modulo timezone and
+ * DST changes).
+ *
+ * Note that if the date is an invalid date (its internal date parameter is a
+ * NaN value), this method throws an error instead of generating an invalid
+ * string.
+ *
+ * @public
+ * @param {Date} date The date to be added to the output string.
+ */
+HeaderEmitter.prototype.addDate = function (date) {
+  // Rather than make a header plastered with NaN values, throw an error on
+  // specific invalid dates.
+  if (isNaN(date.getTime()))
+    throw new Error("Cannot encode an invalid date");
+
+  // RFC 5322 says years can't be before 1900. The after 9999 is a bit that
+  // derives from the specification saying that years have 4 digits.
+  if (date.getFullYear() < 1900 || date.getFullYear() > 9999)
+    throw new Error("Date year is out of encodable range");
+
+  // Start by computing the timezone offset for a day. We lack a good format, so
+  // the the 0-padding is done by hand. Note that the tzoffset we output is in
+  // the form ±hhmm, so we need to separate the offset (in minutes) into an hour
+  // and minute pair.
+  let tzOffset = date.getTimezoneOffset();
+  let tzOffHours = Math.abs(Math.trunc(tzOffset / 60));
+  let tzOffMinutes = Math.abs(tzOffset) % 60;
+  let tzOffsetStr = (tzOffset > 0 ? "-" : "+") +
+    padTo2Digits(tzOffHours) + padTo2Digits(tzOffMinutes);
+
+  // Convert the day-time figure into a single value to avoid unwanted line
+  // breaks in the middle.
+  let dayTime = [
+    kDaysOfWeek[date.getDay()] + ",",
+    date.getDate(),
+    mimeutils.kMonthNames[date.getMonth()],
+    date.getFullYear(),
+    padTo2Digits(date.getHours()) + ":" +
+      padTo2Digits(date.getMinutes()) + ":" +
+      padTo2Digits(date.getSeconds()),
+    tzOffsetStr
+  ].join(" ");
+  this.addText(dayTime, false);
+};
+
 /**
  * Signal that the current header has been finished encoding.
  *
  * @public
  * @param {Boolean} deliverEOF If true, signal to the handler that no more text
  *                             will be arriving.
  */
 HeaderEmitter.prototype.finish = function (deliverEOF) {
--- a/mailnews/mime/jsmime/test/head_xpcshell_glue.js
+++ b/mailnews/mime/jsmime/test/head_xpcshell_glue.js
@@ -48,17 +48,23 @@ var fs = {
 requireCache.set("fs", fs);
 Services.scriptloader.loadSubScript("resource:///modules/jsmime/jsmime.js");
 requireCache.set("jsmime", jsmime);
 
 function require(path) {
   if (requireCache.has(path))
     return requireCache.get(path);
 
-  var file = "resource:///modules/jsmime/" + path + ".js";
+  if (path.startsWith("test/")) {
+    let name = path.substring("test/".length);
+    var file = "resource://testing-common/jsmime/" + name + ".js";
+  } else {
+    var file = "resource:///modules/jsmime/" + path + ".js";
+  }
+
   var globalObject = {
     define: innerDefine.bind(this, path),
   };
   Services.scriptloader.loadSubScript(file, globalObject);
   return requireCache.get(path);
 }
 
 function innerDefine(moduleName, dfn) {
new file mode 100644
--- /dev/null
+++ b/mailnews/mime/jsmime/test/mock_date.js
@@ -0,0 +1,72 @@
+"use strict";
+define(function (require) {
+
+/**
+ * A class which appears to act like the Date class with customizable timezone
+ * offsets.
+ * @param {String} iso8601String An ISO-8601 date/time string including a
+ *                               timezone offset.
+ */
+function MockDate(iso8601String) {
+  // Find the timezone offset (Z or ±hhmm) from the ISO-8601 date string, and
+  // then convert that into a number of minutes.
+  let parse = /\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(Z|[+-]\d{4})/.exec(iso8601String);
+  let tzOffsetStr = parse[1];
+  if (tzOffsetStr == 'Z')
+    this._tzOffset = 0;
+  else {
+    this._tzOffset = parseInt(tzOffsetStr.substring(1, 3)) * 60 +
+      parseInt(tzOffsetStr.substring(3));
+    if (tzOffsetStr[0] == '-')
+      this._tzOffset = -this._tzOffset;
+  }
+
+  // To store the offset, we store both the real time in _realDate and a time
+  // that is offset by the tzOffset in _shiftedDate. Only the getUTC* methods
+  // should be used on these properties, to avoid problems caused by daylight
+  // savings time or other timezone effects. This shifting is always legal
+  // because ES6 is specified to assume that leap seconds do not exist, so there
+  // are always 60 seconds in a minute.
+  this._realDate = new Date(iso8601String);
+  this._shiftedDate = new Date(this._realDate.getTime() +
+    this._tzOffset * 60 * 1000);
+}
+MockDate.prototype = {
+  getTimezoneOffset: function () {
+    // This property is reversed from how it's defined in ISO 8601, i.e.,
+    // UTC +0100 needs to return -60.
+    return -this._tzOffset;
+  },
+  getTime: function () {
+    return this._realDate.getTime();
+  }
+};
+
+// Provide an implementation of Date methods that will be need in JSMime. For
+// the time being, we only need .get* methods.
+for (let name of Object.getOwnPropertyNames(Date.prototype)) {
+  // Only copy getters, not setters or x.toString.
+  if (!name.startsWith('get'))
+    continue;
+  // No redefining any other names on MockDate.
+  if (MockDate.prototype.hasOwnProperty(name))
+    continue;
+
+  if (name.contains('UTC')) {
+    // 'name' is already supposed to be freshly bound per newest ES6 drafts, but
+    // current ES6 implementations reuse the bindings. Until implementations
+    // catch up, use a new let to bind it freshly.
+    let boundName = name;
+    Object.defineProperty(MockDate.prototype, name, { value: function () {
+      return Date.prototype[boundName].call(this._realDate, arguments);
+    }});
+  } else {
+    let newName = 'getUTC' + name.substr(3);
+    Object.defineProperty(MockDate.prototype, name, { value: function () {
+      return Date.prototype[newName].call(this._shiftedDate, arguments);
+    }});
+  }
+}
+
+return MockDate;
+});
--- a/mailnews/mime/jsmime/test/test_header.js
+++ b/mailnews/mime/jsmime/test/test_header.js
@@ -323,16 +323,17 @@ suite('headerparser', function () {
          {name: "extra", group: [{name: "", email: "a@foo.invalid"}]}]],
       ["a < <a@b.c>", [{name: "a", email: "a@b.c"}]],
       ["Name <incomplete@email", [{name: "Name", email: "incomplete@email"}]],
       ["Name <space here@email.invalid>",
         [{name: 'Name', email: '"space here"@email.invalid'}]],
       ["Name <not an email>", [{name: "Name", email: "not an email"}]],
       ["=?UTF-8?Q?Simple?= <a@b.c>",
         [{name: "=?UTF-8?Q?Simple?=", email: "a@b.c"}]],
+      ["No email address", [{name: "No email address", email: ""}]],
     ];
     header_tests.forEach(function (data) {
       arrayTest(data, function () {
         assert.deepEqual(headerparser.parseAddressingHeader(data[0], false),
           data[1]);
       });
     });
   });
@@ -352,16 +353,112 @@ suite('headerparser', function () {
     ];
     header_tests.forEach(function (data) {
       arrayTest(data, function () {
         assert.deepEqual(headerparser.parseAddressingHeader(data[0], true),
           data[1]);
       });
     });
   });
+  suite('parseDateHeader', function () {
+    let header_tests = [
+      // Some basic tests, derived from searching for Date headers in a mailing
+      // list archive.
+      ["Thu, 06 Sep 2012 08:08:21 -0700", "2012-09-06T08:08:21-0700"],
+      ["Thu, 6 Sep 2012 14:49:05 -0400", "2012-09-06T14:49:05-0400"],
+      ["Fri, 07 Sep 2012 07:30:11 -0700 (PDT)", "2012-09-07T07:30:11-0700"],
+      ["9 Sep 2012 21:03:59 -0000", "2012-09-09T21:03:59Z"],
+      ["Sun, 09 Sep 2012 19:10:59 -0400", "2012-09-09T19:10:59-0400"],
+      ["Wed, 17 Jun 2009 10:12:25 +0530", "2009-06-17T10:12:25+0530"],
+
+      // Exercise all the months.
+      ["Mon, 28 Jan 2013 13:35:05 -0500", "2013-01-28T13:35:05-0500"],
+      ["Wed, 29 Feb 2012 23:43:26 +0000", "2012-02-29T23:43:26+0000"],
+      ["Sat, 09 Mar 2013 18:24:47 -0500", "2013-03-09T18:24:47-0500"],
+      ["Sat, 27 Apr 2013 12:51:48 -0400", "2013-04-27T12:51:48-0400"],
+      ["Tue, 28 May 2013 17:21:13 +0800", "2013-05-28T17:21:13+0800"],
+      ["Mon, 17 Jun 2013 22:15:41 +0200", "2013-06-17T22:15:41+0200"],
+      ["Wed, 18 Jul 2012 13:50:47 +0900", "2012-07-18T13:50:47+0900"],
+      ["Mon, 13 Aug 2012 13:55:16 +0200", "2012-08-13T13:55:16+0200"],
+      ["Thu, 06 Sep 2012 19:49:47 -0400", "2012-09-06T19:49:47-0400"],
+      ["Mon, 22 Oct 2012 02:27:23 -0700", "2012-10-22T02:27:23-0700"],
+      ["Thu, 22 Nov 2012 09:04:24 +0800", "2012-11-22T09:04:24+0800"],
+      ["Sun, 25 Dec 2011 12:27:13 +0000", "2011-12-25T12:27:13+0000"],
+
+      // Try out less common timezone offsets.
+      ["Sun, 25 Dec 2011 12:27:13 +1337", "2011-12-25T12:27:13+1337"],
+      ["Sun, 25 Dec 2011 12:27:13 -1337", "2011-12-25T12:27:13-1337"],
+
+      // Leap seconds! Except that since dates in JS don't believe they exist,
+      // they get shoved to the next second.
+      ["30 Jun 2012 23:59:60 +0000", "2012-07-01T00:00:00Z"],
+      ["31 Dec 2008 23:59:60 +0000", "2009-01-01T00:00:00Z"],
+      // This one doesn't exist (they are added only as needed on an irregular
+      // basis), but it's plausible...
+      ["30 Jun 2030 23:59:60 +0000", "2030-07-01T00:00:00Z"],
+      // ... and this one isn't.
+      ["10 Jun 2030 13:39:60 +0000", "2030-06-10T13:40:00Z"],
+      // How about leap seconds in other timezones?
+      ["30 Jun 2012 18:59:60 -0500", "2012-07-01T00:00:00Z"],
+
+      // RFC 5322 obsolete date productions
+      ["Sun, 26 Jan 14 17:14:22 -0600", "2014-01-26T17:14:22-0600"],
+      ["Tue, 26 Jan 49 17:14:22 -0600", "2049-01-26T17:14:22-0600"],
+      ["Thu, 26 Jan 50 17:14:22 -0600", "1950-01-26T17:14:22-0600"],
+      ["Sun, 26 Jan 2014 17:14:22 EST", "2014-01-26T17:14:22-0500"],
+      ["Sun, 26 Jan 2014 17:14:22 CST", "2014-01-26T17:14:22-0600"],
+      ["Sun, 26 Jan 2014 17:14:22 MST", "2014-01-26T17:14:22-0700"],
+      ["Sun, 26 Jan 2014 17:14:22 PST", "2014-01-26T17:14:22-0800"],
+      ["Sun, 26 Jan 2014 17:14:22 AST", "2014-01-26T17:14:22-0400"],
+      ["Sun, 26 Jan 2014 17:14:22 NST", "2014-01-26T17:14:22-0330"],
+      ["Sun, 26 Jan 2014 17:14:22 MET", "2014-01-26T17:14:22+0100"],
+      ["Sun, 26 Jan 2014 17:14:22 EET", "2014-01-26T17:14:22+0200"],
+      ["Sun, 26 Jan 2014 17:14:22 JST", "2014-01-26T17:14:22+0900"],
+      ["Sun, 26 Jan 2014 17:14:22 GMT", "2014-01-26T17:14:22+0000"],
+      ["Sun, 26 Jan 2014 17:14:22 UT", "2014-01-26T17:14:22+0000"],
+      // Daylight savings timezones, even though these aren't actually during
+      // daylight savings time for the relevant jurisdictions.
+      ["Sun, 26 Jan 2014 17:14:22 EDT", "2014-01-26T17:14:22-0400"],
+      ["Sun, 26 Jan 2014 17:14:22 CDT", "2014-01-26T17:14:22-0500"],
+      ["Sun, 26 Jan 2014 17:14:22 MDT", "2014-01-26T17:14:22-0600"],
+      ["Sun, 26 Jan 2014 17:14:22 PDT", "2014-01-26T17:14:22-0700"],
+      ["Sun, 26 Jan 2014 17:14:22 BST", "2014-01-26T17:14:22+0100"],
+      // Unknown time zone--assume UTC
+      ["Sun, 26 Jan 2014 17:14:22 QMT", "2014-01-26T17:14:22+0000"],
+
+      // The following days of the week are incorrect.
+      ["Tue, 28 Jan 2013 13:35:05 -0500", "2013-01-28T13:35:05-0500"],
+      ["Thu, 26 Jan 14 17:14:22 -0600", "2014-01-26T17:14:22-0600"],
+      ["Fri, 26 Jan 49 17:14:22 -0600", "2049-01-26T17:14:22-0600"],
+      ["Mon, 26 Jan 50 17:14:22 -0600", "1950-01-26T17:14:22-0600"],
+      // And for these 2 digit years, they are correct for the other century.
+      ["Mon, 26 Jan 14 17:14:22 -0600", "2014-01-26T17:14:22-0600"],
+      ["Wed, 26 Jan 49 17:14:22 -0600", "2049-01-26T17:14:22-0600"],
+      ["Wed, 26 Jan 50 17:14:22 -0600", "1950-01-26T17:14:22-0600"],
+
+      // Try with some illegal names for days of the week or months of the year.
+      ["Sam, 05 Apr 2014 15:04:13 -0500", "2014-04-05T15:04:13-0500"],
+      ["Lun, 01 Apr 2014 15:04:13 -0500", "2014-04-01T15:04:13-0500"],
+      ["Mar, 02 Apr 2014 15:04:13 -0500", "2014-04-02T15:04:13-0500"],
+      ["Mar, 02 April 2014 15:04:13 -0500", "2014-04-02T15:04:13-0500"],
+      ["Mar, 02 Avr 2014 15:04:13 -0500", NaN],
+      ["Tue, 02 A 2014 15:04:13 -0500", NaN],
+
+
+      // A truly invalid date
+      ["Coincident with the rapture", NaN]
+    ];
+    header_tests.forEach(function (data) {
+      arrayTest(data, function () {
+        assert.equal(headerparser.parseDateHeader(data[0]).toString(),
+          new Date(data[1]).toString());
+      });
+    });
+  });
+
   suite('decodeRFC2047Words', function () {
     let header_tests = [
       // Some basic sanity tests for the test process
       ["Test", "Test"],
       ["Test 2", "Test 2"],
       ["Basic  words", "Basic  words"],
       ["Not a =? word", "Not a =? word"],
 
--- a/mailnews/mime/jsmime/test/test_header_emitter.js
+++ b/mailnews/mime/jsmime/test/test_header_emitter.js
@@ -1,13 +1,15 @@
 "use strict";
 define(function(require) {
 
 var assert = require('assert');
-var headeremitter = require('jsmime').headeremitter;
+var jsmime = require('jsmime');
+var headeremitter = jsmime.headeremitter;
+var MockDate = require('test/mock_date');
 
 function arrayTest(data, fn) {
   fn.toString = function () {
     let text = Function.prototype.toString.call(this);
     text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
       return JSON.stringify(data[p]);
     });
     return text;
@@ -52,16 +54,17 @@ suite('headeremitter', function () {
       [[{name: "", email: "\"hi!bad\"@all.com"}], "\"hi!bad\"@all.com"],
       [[{name: "Doe, John", email: "a@a.com"}], "\"Doe, John\" <a@a.com>"],
       // This one violates the line length, so it underquotes instead.
       [[{name: "A really, really long name to quote", email: "a@example.com"}],
         "A \"really,\" really long name\r\n to quote <a@example.com>"],
       [[{name: "Group", group: [{name: "", email: "a@a.c"},
                                 {name: "", email: "b@b.c"}]}],
         "Group: a@a.c, b@b.c;"],
+      [[{name: "No email address", email: ""}], "No email address"],
     ];
     header_tests.forEach(function (data) {
       arrayTest(data, function () {
         let emitter = headeremitter.makeStreamingEmitter(handler, {
           softMargin: 30,
           useASCII: false,
         });
         handler.reset(data[1]);
@@ -177,16 +180,136 @@ suite('headeremitter', function () {
           useASCII: true
         });
         handler.reset(data[1]);
         emitter.addUnstructured(data[0]);
         emitter.finish(true);
       });
     });
   });
+  suite("addDate", function () {
+    let handler = {
+      reset: function (expected) {
+        this.output = '';
+        this.expected = expected;
+      },
+      deliverData: function (data) { this.output += data; },
+      deliverEOF: function () {
+        assert.equal(this.output, this.expected + '\r\n');
+      }
+    };
+    let header_tests = [
+      // Test basic day/month names
+      ["2000-01-01T00:00:00Z", "Sat, 1 Jan 2000 00:00:00 +0000"],
+      ["2000-02-01T00:00:00Z", "Tue, 1 Feb 2000 00:00:00 +0000"],
+      ["2000-03-01T00:00:00Z", "Wed, 1 Mar 2000 00:00:00 +0000"],
+      ["2000-04-01T00:00:00Z", "Sat, 1 Apr 2000 00:00:00 +0000"],
+      ["2000-05-01T00:00:00Z", "Mon, 1 May 2000 00:00:00 +0000"],
+      ["2000-06-01T00:00:00Z", "Thu, 1 Jun 2000 00:00:00 +0000"],
+      ["2000-07-01T00:00:00Z", "Sat, 1 Jul 2000 00:00:00 +0000"],
+      ["2000-08-01T00:00:00Z", "Tue, 1 Aug 2000 00:00:00 +0000"],
+      ["2000-09-01T00:00:00Z", "Fri, 1 Sep 2000 00:00:00 +0000"],
+      ["2000-10-01T00:00:00Z", "Sun, 1 Oct 2000 00:00:00 +0000"],
+      ["2000-11-01T00:00:00Z", "Wed, 1 Nov 2000 00:00:00 +0000"],
+      ["2000-12-01T00:00:00Z", "Fri, 1 Dec 2000 00:00:00 +0000"],
+
+      // Test timezone offsets
+      ["2000-06-01T12:00:00Z", "Thu, 1 Jun 2000 12:00:00 +0000"],
+      ["2000-06-01T12:00:00+0100", "Thu, 1 Jun 2000 12:00:00 +0100"],
+      ["2000-06-01T12:00:00+0130", "Thu, 1 Jun 2000 12:00:00 +0130"],
+      ["2000-06-01T12:00:00-0100", "Thu, 1 Jun 2000 12:00:00 -0100"],
+      ["2000-06-01T12:00:00-0130", "Thu, 1 Jun 2000 12:00:00 -0130"],
+      ["2000-06-01T12:00:00+1345", "Thu, 1 Jun 2000 12:00:00 +1345"],
+      ["2000-06-01T12:00:00-1200", "Thu, 1 Jun 2000 12:00:00 -1200"],
+      ["2000-06-01T12:00:00+1337", "Thu, 1 Jun 2000 12:00:00 +1337"],
+      ["2000-06-01T12:00:00+0101", "Thu, 1 Jun 2000 12:00:00 +0101"],
+      ["2000-06-01T12:00:00-1337", "Thu, 1 Jun 2000 12:00:00 -1337"],
+
+      // Try some varying hour, minute, and second amounts, to double-check
+      // padding and time dates.
+      ["2000-06-01T01:02:03Z", "Thu, 1 Jun 2000 01:02:03 +0000"],
+      ["2000-06-01T23:13:17Z", "Thu, 1 Jun 2000 23:13:17 +0000"],
+      ["2000-06-01T00:05:04Z", "Thu, 1 Jun 2000 00:05:04 +0000"],
+      ["2000-06-01T23:59:59Z", "Thu, 1 Jun 2000 23:59:59 +0000"],
+      ["2000-06-01T13:17:40Z", "Thu, 1 Jun 2000 13:17:40 +0000"],
+      ["2000-06-01T11:15:34Z", "Thu, 1 Jun 2000 11:15:34 +0000"],
+      ["2000-06-01T04:09:09Z", "Thu, 1 Jun 2000 04:09:09 +0000"],
+      ["2000-06-01T04:10:10Z", "Thu, 1 Jun 2000 04:10:10 +0000"],
+      ["2000-06-01T09:13:17Z", "Thu, 1 Jun 2000 09:13:17 +0000"],
+      ["2000-06-01T13:12:14Z", "Thu, 1 Jun 2000 13:12:14 +0000"],
+      ["2000-06-01T14:16:48Z", "Thu, 1 Jun 2000 14:16:48 +0000"],
+
+      // Try varying month, date, and year values.
+      ["2000-01-31T00:00:00Z", "Mon, 31 Jan 2000 00:00:00 +0000"],
+      ["2000-02-28T00:00:00Z", "Mon, 28 Feb 2000 00:00:00 +0000"],
+      ["2000-02-29T00:00:00Z", "Tue, 29 Feb 2000 00:00:00 +0000"],
+      ["2001-02-28T00:00:00Z", "Wed, 28 Feb 2001 00:00:00 +0000"],
+      ["2000-03-31T00:00:00Z", "Fri, 31 Mar 2000 00:00:00 +0000"],
+      ["2000-04-30T00:00:00Z", "Sun, 30 Apr 2000 00:00:00 +0000"],
+      ["2000-05-31T00:00:00Z", "Wed, 31 May 2000 00:00:00 +0000"],
+      ["2000-06-30T00:00:00Z", "Fri, 30 Jun 2000 00:00:00 +0000"],
+      ["2000-07-31T00:00:00Z", "Mon, 31 Jul 2000 00:00:00 +0000"],
+      ["2000-08-31T00:00:00Z", "Thu, 31 Aug 2000 00:00:00 +0000"],
+      ["2000-09-30T00:00:00Z", "Sat, 30 Sep 2000 00:00:00 +0000"],
+      ["2000-10-31T00:00:00Z", "Tue, 31 Oct 2000 00:00:00 +0000"],
+      ["2000-11-30T00:00:00Z", "Thu, 30 Nov 2000 00:00:00 +0000"],
+      ["2000-12-31T00:00:00Z", "Sun, 31 Dec 2000 00:00:00 +0000"],
+      ["1900-01-01T00:00:00Z", "Mon, 1 Jan 1900 00:00:00 +0000"],
+      ["9999-12-31T23:59:59Z", "Fri, 31 Dec 9999 23:59:59 +0000"],
+
+      // Tests that are not actually missing:
+      // We don't actually need to test daylight savings time issues, so long as
+      // getTimezoneOffset is correct. We've confirmed black-box that the value
+      // is being directly queried on every instance, since we have tests that
+      // make MockDate.getTimezoneOffset return different values.
+      // In addition, ES6 Date objects don't support leap seconds. Invalid dates
+      // per RFC 5322 are handled in a later run of code.
+    ];
+    header_tests.forEach(function (data) {
+      arrayTest(data, function () {
+        let emitter = headeremitter.makeStreamingEmitter(handler, { });
+        handler.reset(data[1]);
+        emitter.addDate(new MockDate(data[0]));
+        emitter.finish(true);
+      });
+    });
+
+    // An invalid date should throw an error instead of make a malformed header.
+    test('Invalid dates', function () {
+      let emitter = headeremitter.makeStreamingEmitter(handler, { });
+      assert.throws(function () { emitter.addDate(new Date(NaN)); });
+      assert.throws(function () { emitter.addDate(new Date("1850-01-01")); });
+      assert.throws(function () { emitter.addDate(new Date("10000-01-01")); });
+    });
+
+    // Test preferred breaking for the date header.
+    test('Break spot', function () {
+      let emitter = headeremitter.makeStreamingEmitter(handler, {
+        softMargin: 30
+      });
+      handler.reset("Overly-Long-Date:\r\n Sat, 1 Jan 2000 00:00:00 +0000");
+      emitter.addHeaderName("Overly-Long-Date");
+      emitter.addDate(new MockDate("2000-01-01T00:00:00Z"));
+      emitter.finish();
+    });
+
+    test('Correctness of date', function () {
+      let emitter = headeremitter.makeStreamingEmitter(handler, { });
+      handler.reset();
+      let now = new Date();
+      emitter.addDate(now);
+      emitter.finish();
+      // All engines can parse the date strings we produce
+      let reparsed = new Date(handler.output);
+
+      // Now and reparsed should be correct to second-level precision.
+      assert.equal(reparsed.getMilliseconds(), 0);
+      assert.equal(now.getTime() - now.getMilliseconds(), reparsed.getTime());
+    });
+  });
 
   suite("Header lengths", function () {
     let handler = {
       reset: function (expected) {
         this.output = '';
         this.expected = expected;
       },
       deliverData: function (data) { this.output += data; },
--- a/mailnews/mime/jsmime/test/test_structured_header_emitters.js
+++ b/mailnews/mime/jsmime/test/test_structured_header_emitters.js
@@ -1,13 +1,14 @@
 "use strict";
 define(function (require) {
 
 var assert = require('assert');
 var headeremitter = require('jsmime').headeremitter;
+var MockDate = require('test/mock_date');
 
 function arrayTest(data, fn) {
   fn.toString = function () {
     let text = Function.prototype.toString.call(this);
     text = text.replace(/data\[([0-9]*)\]/g, function (m, p) {
       return JSON.stringify(data[p]);
     });
     return text;
@@ -50,16 +51,25 @@ suite('Structured header emitters', func
       "John Doe <john.doe@test.invalid>"],
     [{name: "undisclosed-recipients", group: []},
       "undisclosed-recipients: ;"],
   ];
   addressing_headers.forEach(function (header) {
     testHeader(header, address_tests);
   });
 
+  let date_headers = ['Date', 'Expires', 'Injection-Date', 'NNTP-Posting-Date',
+    'Resent-Date'];
+  let date_tests = [
+    [new MockDate("2012-09-06T08:08:21-0700"), "Thu, 6 Sep 2012 08:08:21 -0700"],
+  ];
+  date_headers.forEach(function (header) {
+    testHeader(header, date_tests);
+  });
+
   let unstructured_headers = ['Comments', 'Content-Description', 'Keywords',
     'Subject'];
   let unstructured_tests = [
     ["", ""],
     ["This is a subject", "This is a subject"],
     ["\u79c1\u306f\u4ef6\u540d\u5348\u524d",
       "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?="],
   ];
--- a/mailnews/mime/jsmime/test/test_structured_headers.js
+++ b/mailnews/mime/jsmime/test/test_structured_headers.js
@@ -122,16 +122,26 @@ suite('Structured headers', function () 
     [["a@example.invalid", "b@example.invalid"],
       [{name: "", email: "a@example.invalid"},
        {name: "", email: "b@example.invalid"}]],
   ];
   addressing_headers.forEach(function (header) {
     testHeader(header, address_tests);
   });
 
+  let date_headers = ['Date', 'Expires', 'Injection-Date', 'NNTP-Posting-Date',
+    'Resent-Date'];
+  let date_tests = [
+    ["Thu, 06 Sep 2012 08:08:21 -0700", new Date("2012-09-06T08:08:21-0700")],
+    ["This is so not a date", new Date(NaN)],
+  ];
+  date_headers.forEach(function (header) {
+    testHeader(header, date_tests);
+  });
+
   let unstructured_headers = ['Comments', 'Content-Description', 'Keywords',
     'Subject'];
   let unstructured_tests = [
     ["", ""],
     ["This is a subject", "This is a subject"],
     [["Subject 1", "Subject 2"], "Subject 1"],
     ["=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?=",
       "\u79c1\u306f\u4ef6\u540d\u5348\u524d"],
--- a/mailnews/mime/moz.build
+++ b/mailnews/mime/moz.build
@@ -11,9 +11,13 @@ DIRS += [
 ]
 
 TEST_DIRS += ['test']
 
 EXTRA_JS_MODULES.jsmime += [
     'jsmime/jsmime.js',
 ]
 
+TESTING_JS_MODULES.jsmime += [
+    'jsmime/test/mock_date.js',
+]
+
 XPCSHELL_TESTS_MANIFESTS += ['jsmime/test/xpcshell.ini']