summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/MessageGenerator.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/test/resources/MessageGenerator.jsm')
-rw-r--r--comm/mailnews/test/resources/MessageGenerator.jsm1651
1 files changed, 1651 insertions, 0 deletions
diff --git a/comm/mailnews/test/resources/MessageGenerator.jsm b/comm/mailnews/test/resources/MessageGenerator.jsm
new file mode 100644
index 0000000000..4e3f8b75f1
--- /dev/null
+++ b/comm/mailnews/test/resources/MessageGenerator.jsm
@@ -0,0 +1,1651 @@
+/* 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 = [
+ "MessageGenerator",
+ "addMessagesToFolder",
+ "MessageScenarioFactory",
+ "SyntheticPartLeaf",
+ "SyntheticDegeneratePartEmpty",
+ "SyntheticPartMulti",
+ "SyntheticPartMultiMixed",
+ "SyntheticPartMultiParallel",
+ "SyntheticPartMultiDigest",
+ "SyntheticPartMultiAlternative",
+ "SyntheticPartMultiRelated",
+ "SyntheticPartMultiSignedSMIME",
+ "SyntheticPartMultiSignedPGP",
+ "SyntheticMessage",
+ "SyntheticMessageSet",
+];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * A list of first names for use by MessageGenerator to create deterministic,
+ * reversible names. To keep things easily reversible, if you add names, make
+ * sure they have no spaces in them!
+ */
+var FIRST_NAMES = [
+ "Andy",
+ "Bob",
+ "Chris",
+ "David",
+ "Emily",
+ "Felix",
+ "Gillian",
+ "Helen",
+ "Idina",
+ "Johnny",
+ "Kate",
+ "Lilia",
+ "Martin",
+ "Neil",
+ "Olof",
+ "Pete",
+ "Quinn",
+ "Rasmus",
+ "Sarah",
+ "Troels",
+ "Ulf",
+ "Vince",
+ "Will",
+ "Xavier",
+ "Yoko",
+ "Zig",
+];
+
+/**
+ * A list of last names for use by MessageGenerator to create deterministic,
+ * reversible names. To keep things easily reversible, if you add names, make
+ * sure they have no spaces in them!
+ */
+var LAST_NAMES = [
+ "Anway",
+ "Bell",
+ "Clarke",
+ "Davol",
+ "Ekberg",
+ "Flowers",
+ "Gilbert",
+ "Hook",
+ "Ivarsson",
+ "Jones",
+ "Kurtz",
+ "Lowe",
+ "Morris",
+ "Nagel",
+ "Orzabal",
+ "Price",
+ "Quinn",
+ "Rolinski",
+ "Stanley",
+ "Tennant",
+ "Ulvaeus",
+ "Vannucci",
+ "Wiggs",
+ "Xavier",
+ "Young",
+ "Zig",
+];
+
+/**
+ * A list of adjectives used to construct a deterministic, reversible subject
+ * by MessageGenerator. To keep things easily reversible, if you add more,
+ * make sure they have no spaces in them! Also, make sure your additions
+ * don't break the secret Monty Python reference!
+ */
+var SUBJECT_ADJECTIVES = [
+ "Big",
+ "Small",
+ "Huge",
+ "Tiny",
+ "Red",
+ "Green",
+ "Blue",
+ "My",
+ "Happy",
+ "Sad",
+ "Grumpy",
+ "Angry",
+ "Awesome",
+ "Fun",
+ "Lame",
+ "Funky",
+];
+
+/**
+ * A list of nouns used to construct a deterministic, reversible subject
+ * by MessageGenerator. To keep things easily reversible, if you add more,
+ * make sure they have no spaces in them! Also, make sure your additions
+ * don't break the secret Monty Python reference!
+ */
+var SUBJECT_NOUNS = [
+ "Meeting",
+ "Party",
+ "Shindig",
+ "Wedding",
+ "Document",
+ "Report",
+ "Spreadsheet",
+ "Hovercraft",
+ "Aardvark",
+ "Giraffe",
+ "Llama",
+ "Velociraptor",
+ "Laser",
+ "Ray-Gun",
+ "Pen",
+ "Sword",
+];
+
+/**
+ * A list of suffixes used to construct a deterministic, reversible subject
+ * by MessageGenerator. These can (clearly) have spaces in them. Make sure
+ * your additions don't break the secret Monty Python reference!
+ */
+var SUBJECT_SUFFIXES = [
+ "Today",
+ "Tomorrow",
+ "Yesterday",
+ "In a Fortnight",
+ "Needs Attention",
+ "Very Important",
+ "Highest Priority",
+ "Full Of Eels",
+ "In The Lobby",
+ "On Your Desk",
+ "In Your Car",
+ "Hiding Behind The Door",
+];
+
+/**
+ * Base class for MIME Part representation.
+ */
+function SyntheticPart(aProperties) {
+ if (aProperties) {
+ if ("contentType" in aProperties) {
+ this._contentType = aProperties.contentType;
+ }
+ if ("charset" in aProperties) {
+ this._charset = aProperties.charset;
+ }
+ if ("format" in aProperties) {
+ this._format = aProperties.format;
+ }
+ if ("filename" in aProperties) {
+ this._filename = aProperties.filename;
+ }
+ if ("boundary" in aProperties) {
+ this._boundary = aProperties.boundary;
+ }
+ if ("encoding" in aProperties) {
+ this._encoding = aProperties.encoding;
+ }
+ if ("contentId" in aProperties) {
+ this._contentId = aProperties.contentId;
+ }
+ if ("disposition" in aProperties) {
+ this._forceDisposition = aProperties.disposition;
+ }
+ if ("extraHeaders" in aProperties) {
+ this._extraHeaders = aProperties.extraHeaders;
+ }
+ }
+}
+SyntheticPart.prototype = {
+ _forceDisposition: null,
+ _encoding: null,
+
+ get contentTypeHeaderValue() {
+ let s = this._contentType;
+ if (this._charset) {
+ s += "; charset=" + this._charset;
+ }
+ if (this._format) {
+ s += "; format=" + this._format;
+ }
+ if (this._filename) {
+ s += ';\r\n name="' + this._filename + '"';
+ }
+ if (this._contentTypeExtra) {
+ for (let [key, value] of Object.entries(this._contentTypeExtra)) {
+ s += ";\r\n " + key + '="' + value + '"';
+ }
+ }
+ if (this._boundary) {
+ s += ';\r\n boundary="' + this._boundary + '"';
+ }
+ return s;
+ },
+ get hasTransferEncoding() {
+ return this._encoding;
+ },
+ get contentTransferEncodingHeaderValue() {
+ return this._encoding;
+ },
+ get hasDisposition() {
+ return this._forceDisposition || this._filename || false;
+ },
+ get contentDispositionHeaderValue() {
+ let s = "";
+ if (this._forceDisposition) {
+ s += this._forceDisposition;
+ } else if (this._filename) {
+ s += 'attachment;\r\n filename="' + this._filename + '"';
+ }
+ return s;
+ },
+ get hasContentId() {
+ return this._contentId || false;
+ },
+ get contentIdHeaderValue() {
+ return "<" + this._contentId + ">";
+ },
+ get hasExtraHeaders() {
+ return this._extraHeaders || false;
+ },
+ get extraHeaders() {
+ return this._extraHeaders || false;
+ },
+};
+
+/**
+ * Leaf MIME part, defaulting to text/plain.
+ */
+function SyntheticPartLeaf(aBody, aProperties) {
+ SyntheticPart.call(this, aProperties);
+ this.body = aBody;
+}
+SyntheticPartLeaf.prototype = {
+ __proto__: SyntheticPart.prototype,
+ _contentType: "text/plain",
+ _charset: "ISO-8859-1",
+ _format: "flowed",
+ _encoding: "7bit",
+ toMessageString() {
+ return this.body;
+ },
+ prettyString(aIndent) {
+ return "Leaf: " + this._contentType;
+ },
+};
+
+/**
+ * A part that tells us to produce NO output in a multipart section. So if our
+ * separator is "--BOB", we might produce "--BOB\n--BOB--\n" instead of having
+ * some headers and actual content in there.
+ * This is not a good idea and probably not legal either, but it happens and
+ * we need to test for it.
+ */
+function SyntheticDegeneratePartEmpty() {}
+SyntheticDegeneratePartEmpty.prototype = {
+ prettyString(aIndent) {
+ return "Degenerate Empty Part";
+ },
+};
+
+/**
+ * Multipart (multipart/*) MIME part base class.
+ */
+function SyntheticPartMulti(aParts, aProperties) {
+ SyntheticPart.call(this, aProperties);
+
+ this._boundary = "--------------CHOPCHOP" + this.BOUNDARY_COUNTER;
+ this.BOUNDARY_COUNTER_HOME.BOUNDARY_COUNTER += 1;
+ this.parts = aParts != null ? aParts : [];
+}
+SyntheticPartMulti.prototype = {
+ __proto__: SyntheticPart.prototype,
+ BOUNDARY_COUNTER: 0,
+ toMessageString() {
+ let s = "This is a multi-part message in MIME format.\r\n";
+ for (let part of this.parts) {
+ s += "--" + this._boundary + "\r\n";
+ if (part instanceof SyntheticDegeneratePartEmpty) {
+ continue;
+ }
+ s += "Content-Type: " + part.contentTypeHeaderValue + "\r\n";
+ if (part.hasTransferEncoding) {
+ s +=
+ "Content-Transfer-Encoding: " +
+ part.contentTransferEncodingHeaderValue +
+ "\r\n";
+ }
+ if (part.hasDisposition) {
+ s +=
+ "Content-Disposition: " + part.contentDispositionHeaderValue + "\r\n";
+ }
+ if (part.hasContentId) {
+ s += "Content-ID: " + part.contentIdHeaderValue + "\r\n";
+ }
+ if (part.hasExtraHeaders) {
+ for (let k in part.extraHeaders) {
+ let v = part.extraHeaders[k];
+ s += k + ": " + v + "\r\n";
+ }
+ }
+ s += "\r\n";
+ s += part.toMessageString() + "\r\n";
+ }
+ s += "--" + this._boundary + "--";
+ return s;
+ },
+ prettyString(aIndent) {
+ let nextIndent = aIndent != null ? aIndent + " " : "";
+
+ let s = "Container: " + this._contentType;
+
+ for (let iPart = 0; iPart < this.parts.length; iPart++) {
+ let part = this.parts[iPart];
+ s +=
+ "\n" + nextIndent + (iPart + 1) + " " + part.prettyString(nextIndent);
+ }
+
+ return s;
+ },
+};
+SyntheticPartMulti.prototype.BOUNDARY_COUNTER_HOME =
+ SyntheticPartMulti.prototype;
+
+/**
+ * Multipart mixed (multipart/mixed) MIME part.
+ */
+function SyntheticPartMultiMixed(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiMixed.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/mixed",
+};
+
+/**
+ * Multipart mixed (multipart/mixed) MIME part.
+ */
+function SyntheticPartMultiParallel(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiParallel.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/parallel",
+};
+
+/**
+ * Multipart digest (multipart/digest) MIME part.
+ */
+function SyntheticPartMultiDigest(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiDigest.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/digest",
+};
+
+/**
+ * Multipart alternative (multipart/alternative) MIME part.
+ */
+function SyntheticPartMultiAlternative(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiAlternative.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/alternative",
+};
+
+/**
+ * Multipart related (multipart/related) MIME part.
+ */
+function SyntheticPartMultiRelated(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiRelated.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/related",
+};
+
+var PKCS_SIGNATURE_MIME_TYPE = "application/x-pkcs7-signature";
+/**
+ * Multipart signed (multipart/signed) SMIME part. This is helperish and makes
+ * up a gibberish signature. We wrap the provided parts in the standard
+ * signature idiom
+ *
+ * @param {string} aPart - The content part to wrap. Only one part!
+ * Use a multipart if you need to cram extra stuff in there.
+ * @param {object} aProperties - Properties, propagated to SyntheticPart, see that.
+ */
+function SyntheticPartMultiSignedSMIME(aPart, aProperties) {
+ SyntheticPartMulti.call(this, [aPart], aProperties);
+ this.parts.push(
+ new SyntheticPartLeaf(
+ "I am not really a signature but let's hope no one figures it out.",
+ {
+ contentType: PKCS_SIGNATURE_MIME_TYPE,
+ name: "smime.p7s",
+ }
+ )
+ );
+}
+SyntheticPartMultiSignedSMIME.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/signed",
+ _contentTypeExtra: {
+ protocol: PKCS_SIGNATURE_MIME_TYPE,
+ micalg: "SHA1",
+ },
+};
+
+var PGP_SIGNATURE_MIME_TYPE = "application/pgp-signature";
+/**
+ * Multipart signed (multipart/signed) PGP part. This is helperish and makes
+ * up a gibberish signature. We wrap the provided parts in the standard
+ * signature idiom
+ *
+ * @param {string} aPart - The content part to wrap. Only one part!
+ * Use a multipart if you need to cram extra stuff in there.
+ * @param {object} aProperties - Properties, propagated to SyntheticPart, see that.
+ */
+function SyntheticPartMultiSignedPGP(aPart, aProperties) {
+ SyntheticPartMulti.call(this, [aPart], aProperties);
+ this.parts.push(
+ new SyntheticPartLeaf(
+ "I am not really a signature but let's hope no one figures it out.",
+ {
+ contentType: PGP_SIGNATURE_MIME_TYPE,
+ }
+ )
+ );
+}
+SyntheticPartMultiSignedPGP.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/signed",
+ _contentTypeExtra: {
+ protocol: PGP_SIGNATURE_MIME_TYPE,
+ micalg: "pgp-sha1",
+ },
+};
+
+var _DEFAULT_META_STATES = {
+ junk: false,
+ read: false,
+};
+
+/**
+ * A synthetic message, created by the MessageGenerator. Captures both the
+ * ingredients that went into the synthetic message as well as the rfc822 form
+ * of the message.
+ *
+ * @param {object} [aHeaders] A dictionary of rfc822 header payloads.
+ * The key should be capitalized as you want it to appear in the output.
+ * This requires adherence to convention of this class. You are best to just
+ * use the helpers provided by this class.
+ * @param {object} [aBodyPart] - An instance of one of the many Synthetic part
+ * types available in this file.
+ * @param {object} [aMetaState] - A dictionary of meta-state about the message
+ * that is only relevant to the MessageInjection logic and perhaps some
+ * testing logic.
+ * @param {boolean} [aMetaState.junk=false] Is the method junk?
+ */
+function SyntheticMessage(aHeaders, aBodyPart, aMetaState) {
+ // we currently do not need to call SyntheticPart's constructor...
+ this.headers = aHeaders || {};
+ this.bodyPart = aBodyPart || new SyntheticPartLeaf("");
+ this.metaState = aMetaState || {};
+ for (let key in _DEFAULT_META_STATES) {
+ let value = _DEFAULT_META_STATES[key];
+ if (!(key in this.metaState)) {
+ this.metaState[key] = value;
+ }
+ }
+}
+
+SyntheticMessage.prototype = {
+ __proto__: SyntheticPart.prototype,
+ _contentType: "message/rfc822",
+ _charset: null,
+ _format: null,
+ _encoding: null,
+
+ /** @returns {string} The Message-Id header value. */
+ get messageId() {
+ return this._messageId;
+ },
+ /**
+ * Sets the Message-Id header value.
+ *
+ * @param {string} aMessageId - A unique string without the greater-than and
+ * less-than, we add those for you.
+ */
+ set messageId(aMessageId) {
+ this._messageId = aMessageId;
+ this.headers["Message-Id"] = "<" + aMessageId + ">";
+ },
+
+ /** @returns {Date} The message Date header value. */
+ get date() {
+ return this._date;
+ },
+ /**
+ * Sets the Date header to the given javascript Date object.
+ *
+ * @param {Date} aDate The date you want the message to claim to be from.
+ */
+ set date(aDate) {
+ this._date = aDate;
+ let dateParts = aDate.toString().split(" ");
+ this.headers.Date =
+ dateParts[0] +
+ ", " +
+ dateParts[2] +
+ " " +
+ dateParts[1] +
+ " " +
+ dateParts[3] +
+ " " +
+ dateParts[4] +
+ " " +
+ dateParts[5].substring(3);
+ },
+
+ /** @returns {string} The message subject. */
+ get subject() {
+ return this._subject;
+ },
+ /**
+ * Sets the message subject.
+ *
+ * @param {string} aSubject - A string sans newlines or other illegal characters.
+ */
+ set subject(aSubject) {
+ this._subject = aSubject;
+ this.headers.Subject = aSubject;
+ },
+
+ /**
+ * Given a tuple containing [a display name, an e-mail address], returns a
+ * string suitable for use in a to/from/cc header line.
+ *
+ * @param {string[]} aNameAndAddress - A list with two elements. The first
+ * should be the display name (sans wrapping quotes). The second element
+ * should be the e-mail address (sans wrapping greater-than/less-than).
+ */
+ _formatMailFromNameAndAddress(aNameAndAddress) {
+ // if the name is encoded, do not put it in quotes!
+ if (aNameAndAddress[0].startsWith("=")) {
+ return aNameAndAddress[0] + " <" + aNameAndAddress[1] + ">";
+ }
+ return '"' + aNameAndAddress[0] + '" <' + aNameAndAddress[1] + ">";
+ },
+
+ /**
+ * Given a mailbox, parse out name and email. The mailbox
+ * can (per rfc 2822) be of two forms:
+ * 1) Name <me@example.org>
+ * 2) me@example.org
+ *
+ * @returns {string[]} A tuple of name, email.
+ */
+ _parseMailbox(mailbox) {
+ let matcher = mailbox.match(/(.*)<(.+@.+)>/);
+ if (!matcher) {
+ // no match -> second form
+ return ["", mailbox];
+ }
+
+ let name = matcher[1].trim();
+ let email = matcher[2].trim();
+ return [name, email];
+ },
+
+ /** @returns {string[]} The name-and-address tuple used when setting the From header. */
+ get from() {
+ return this._from;
+ },
+ /**
+ * Sets the From header using the given tuple containing [a display name,
+ * an e-mail address].
+ *
+ * @param {string[]} aNameAndAddress - A list with two elements. The first
+ * should be the display name (sans wrapping quotes). The second element
+ * should be the e-mail address (sans wrapping greater-than/less-than).
+ * Can also be a string, should then be a valid raw From: header value.
+ */
+ set from(aNameAndAddress) {
+ if (typeof aNameAndAddress === "string") {
+ this._from = this._parseMailbox(aNameAndAddress);
+ this.headers.From = aNameAndAddress;
+ return;
+ }
+ this._from = aNameAndAddress;
+ this.headers.From = this._formatMailFromNameAndAddress(aNameAndAddress);
+ },
+
+ /** @returns {string} The display name part of the From header. */
+ get fromName() {
+ return this._from[0];
+ },
+ /** @returns {string} The e-mail address part of the From header (no display name). */
+ get fromAddress() {
+ return this._from[1];
+ },
+
+ /**
+ * For our header storage, we may need to pre-add commas, this does it.
+ *
+ * @param {string[]} aList - A list of strings that is mutated so that every
+ * string in the list except the last one has a comma appended to it.
+ */
+ _commaize(aList) {
+ for (let i = 0; i < aList.length - 1; i++) {
+ aList[i] = aList[i] + ",";
+ }
+ return aList;
+ },
+
+ /**
+ * @returns {string[][]} the comma-ized list of name-and-address tuples used
+ * to set the To header.
+ */
+ get to() {
+ return this._to;
+ },
+ /**
+ * Sets the To header using a list of tuples containing [a display name,
+ * an e-mail address].
+ *
+ * @param {string[][]} aNameAndAddresses - A list of name-and-address tuples.
+ * Each tuple is alist with two elements. The first should be the
+ * display name (sans wrapping quotes). The second element should be the
+ * e-mail address (sans wrapping greater-than/less-than).
+ * Can also be a string, should then be a valid raw To: header value.
+ */
+ set to(aNameAndAddresses) {
+ if (typeof aNameAndAddresses === "string") {
+ this._to = [];
+ let people = aNameAndAddresses.split(",");
+ for (let i = 0; i < people.length; i++) {
+ this._to.push(this._parseMailbox(people[i]));
+ }
+
+ this.headers.To = aNameAndAddresses;
+ return;
+ }
+ this._to = aNameAndAddresses;
+ this.headers.To = this._commaize(
+ aNameAndAddresses.map(nameAndAddr =>
+ this._formatMailFromNameAndAddress(nameAndAddr)
+ )
+ );
+ },
+ /** @returns {string} The display name of the first intended recipient. */
+ get toName() {
+ return this._to[0][0];
+ },
+ /** @returns {string} The email address (no display name) of the first recipient. */
+ get toAddress() {
+ return this._to[0][1];
+ },
+
+ /**
+ * @returns {string[][]} The comma-ized list of name-and-address tuples used
+ * to set the Cc header.
+ */
+ get cc() {
+ return this._cc;
+ },
+ /**
+ * Sets the Cc header using a list of tuples containing [a display name,
+ * an e-mail address].
+ *
+ * @param {string[][]} aNameAndAddresses - A list of name-and-address tuples.
+ * Each tuple is a list with two elements. The first should be the
+ * display name (sans wrapping quotes). The second element should be the
+ * e-mail address (sans wrapping greater-than/less-than).
+ * Can also be a string, should then be a valid raw Cc: header value.
+ */
+ set cc(aNameAndAddresses) {
+ if (typeof aNameAndAddresses === "string") {
+ this._cc = [];
+ let people = aNameAndAddresses.split(",");
+ for (let i = 0; i < people.length; i++) {
+ this._cc.push(this._parseMailbox(people[i]));
+ }
+ this.headers.Cc = aNameAndAddresses;
+ return;
+ }
+ this._cc = aNameAndAddresses;
+ this.headers.Cc = this._commaize(
+ aNameAndAddresses.map(nameAndAddr =>
+ this._formatMailFromNameAndAddress(nameAndAddr)
+ )
+ );
+ },
+
+ get bodyPart() {
+ return this._bodyPart;
+ },
+ set bodyPart(aBodyPart) {
+ this._bodyPart = aBodyPart;
+ this.headers["Content-Type"] = this._bodyPart.contentTypeHeaderValue;
+ },
+
+ /**
+ * Normalizes header values, which may be strings or arrays of strings, into
+ * a suitable string suitable for appending to the header name/key.
+ *
+ * @returns {string} A normalized string representation of the header
+ * value(s), which may include spanning multiple lines.
+ */
+ _formatHeaderValues(aHeaderValues) {
+ // may not be an array
+ if (!(aHeaderValues instanceof Array)) {
+ return aHeaderValues;
+ }
+ // it's an array!
+ if (aHeaderValues.length == 1) {
+ return aHeaderValues[0];
+ }
+ return aHeaderValues.join("\r\n\t");
+ },
+
+ /**
+ * @returns {string} A string uniquely identifying this message, at least
+ * as long as the messageId is set and unique.
+ */
+ toString() {
+ return "msg:" + this._messageId;
+ },
+
+ /**
+ * Convert the message and its hierarchy into a "pretty string". The message
+ * and each MIME part get their own line. The string never ends with a
+ * newline. For a non-multi-part message, only a single line will be
+ * returned.
+ * Messages have their subject displayed, everyone else just shows their
+ * content type.
+ */
+ prettyString(aIndent) {
+ if (aIndent === undefined) {
+ aIndent = "";
+ }
+ let nextIndent = aIndent + " ";
+
+ let s = "Message: " + this.subject;
+ s += "\n" + nextIndent + "1 " + this.bodyPart.prettyString(nextIndent);
+
+ return s;
+ },
+
+ /**
+ * @returns {string} This messages in rfc822 format, or something close enough.
+ */
+ toMessageString() {
+ let lines = Object.keys(this.headers).map(
+ headerKey =>
+ headerKey + ": " + this._formatHeaderValues(this.headers[headerKey])
+ );
+
+ return lines.join("\r\n") + "\r\n\r\n" + this.bodyPart.toMessageString();
+ },
+
+ toMboxString() {
+ return "From " + this._from[1] + "\r\n" + this.toMessageString() + "\r\n";
+ },
+
+ /**
+ * @returns {nsIStringInputStream} This message in rfc822 format in a string stream.
+ */
+ toStream() {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ let str = this.toMessageString();
+ stream.setData(str, str.length);
+ return stream;
+ },
+
+ /**
+ * Writes this message to an mbox stream. his means adding a "From " line
+ * and making sure we've got a trailing newline.
+ */
+ writeToMboxStream(aStream) {
+ let str = this.toMboxString();
+ aStream.write(str, str.length);
+ },
+};
+
+/**
+ * Write a list of messages to a folder
+ *
+ * @param {SyntheticMessage[]} aMessages - The list of SyntheticMessages instances to write.
+ * @param {nsIMsgFolder} aFolder - The folder to write to.
+ */
+function addMessagesToFolder(aMessages, aFolder) {
+ let localFolder = aFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ for (let message of aMessages) {
+ localFolder.addMessage(message.toMboxString());
+ }
+}
+
+/**
+ * Represents a set of synthetic messages, also supporting insertion into and
+ * tracking of the message folders to which they belong. This then allows
+ * mutations of the messages (in their folders) for testing purposes.
+ *
+ * In general, you would create a synthetic message set by passing in only a
+ * list of synthetic messages, and then add then messages to nsIMsgFolders by
+ * using one of the addMessage* methods. This will populate the aMsgFolders
+ * and aFolderIndices values. (They are primarily intended for reasons of
+ * slicing, but people who know what they are doing can also use them.)
+ *
+ * @param {SyntheticMessage[]} aSynMessages The synthetic messages that should belong to this set.
+ * @param {nsIMsgFolder|nsIMsgFolder[]} [aMsgFolders] Optional nsIMsgFolder or list of folders.
+ * @param {number[]} [aFolderIndices] Optional list where each value is an index into the
+ * msgFolders attribute, specifying what folder the message can be found
+ * in. The value may also be null if the message has not yet been
+ * inserted into a folder.
+ */
+function SyntheticMessageSet(aSynMessages, aMsgFolders, aFolderIndices) {
+ this.synMessages = aSynMessages;
+
+ if (Array.isArray(aMsgFolders)) {
+ this.msgFolders = aMsgFolders;
+ } else if (aMsgFolders) {
+ this.msgFolders = [aMsgFolders];
+ } else {
+ this.msgFolders = [];
+ }
+ if (aFolderIndices == null) {
+ this.folderIndices = aSynMessages.map(_ => null);
+ } else {
+ this.folderIndices = aFolderIndices;
+ }
+}
+SyntheticMessageSet.prototype = {
+ /**
+ * Helper method for messageInjection to use to tell us it is injecting a
+ * message in a given folder. As a convenience, we also return the
+ * synthetic message.
+ *
+ * @protected
+ */
+ _trackMessageAddition(aFolder, aMessageIndex) {
+ let aFolderIndex = this.msgFolders.indexOf(aFolder);
+ if (aFolderIndex == -1) {
+ aFolderIndex = this.msgFolders.push(aFolder) - 1;
+ }
+ this.folderIndices[aMessageIndex] = aFolderIndex;
+ return this.synMessages[aMessageIndex];
+ },
+ /**
+ * Helper method for use by |MessageInjection.async_move_messages| to tell us that it moved
+ * all the messages from aOldFolder to aNewFolder.
+ */
+ _folderSwap(aOldFolder, aNewFolder) {
+ let folderIndex = this.msgFolders.indexOf(aOldFolder);
+ this.msgFolders[folderIndex] = aNewFolder;
+ },
+
+ /**
+ * Union this set with another set and return the (new) result.
+ *
+ * @param {SyntheticMessageSet} aOtherSet - The other synthetic message set.
+ * @returns {SyntheticMessageSet} A new SyntheticMessageSet containing the
+ * union of this set and the other set.
+ */
+ union(aOtherSet) {
+ let messages = this.synMessages.concat(aOtherSet.synMessages);
+ let folders = this.msgFolders.concat();
+ let indices = this.folderIndices.concat();
+
+ let folderUrisToIndices = {};
+ for (let [iFolder, folder] of this.msgFolders.entries()) {
+ folderUrisToIndices[folder.URI] = iFolder;
+ }
+
+ for (let iOther = 0; iOther < aOtherSet.synMessages.length; iOther++) {
+ let folderIndex = aOtherSet.folderIndices[iOther];
+ if (folderIndex == null) {
+ indices.push(folderIndex);
+ } else {
+ let folder = aOtherSet.msgFolders[folderIndex];
+ if (!(folder.URI in folderUrisToIndices)) {
+ folderUrisToIndices[folder.URI] = folders.length;
+ folders.push(folder);
+ }
+ indices.push(folderUrisToIndices[folder.URI]);
+ }
+ }
+
+ return new SyntheticMessageSet(messages, folders, indices);
+ },
+
+ /**
+ * Get the single message header of the message at the given index; use
+ * |msgHdrs| if you want to get all the headers at once.
+ *
+ * @param {integer} aIndex
+ */
+ getMsgHdr(aIndex) {
+ let folder = this.msgFolders[this.folderIndices[aIndex]];
+ let synMsg = this.synMessages[aIndex];
+ return folder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId);
+ },
+
+ /**
+ * Get the URI for the message at the given index.
+ *
+ * @param {integer} aIndex
+ */
+ getMsgURI(aIndex) {
+ let msgHdr = this.getMsgHdr(aIndex);
+ return msgHdr.folder.getUriForMsg(msgHdr);
+ },
+
+ /**
+ * @yields {nsIMsgDBHdr} A JS iterator of the message headers for all
+ * messages inserted into a folder.
+ */
+ *msgHdrs() {
+ // get the databases
+ let msgDatabases = this.msgFolders.map(folder => folder.msgDatabase);
+ for (let [iMsg, synMsg] of this.synMessages.entries()) {
+ let folderIndex = this.folderIndices[iMsg];
+ if (folderIndex != null) {
+ yield msgDatabases[folderIndex].getMsgHdrForMessageID(synMsg.messageId);
+ }
+ }
+ },
+ /**
+ * @returns {nsIMsgDBHdr} A JS list of the message headers for all
+ * messages inserted into a folder.
+ */
+ get msgHdrList() {
+ return Array.from(this.msgHdrs());
+ },
+
+ /**
+ * @returns {object[]} - A list where each item is a list with two elements;
+ * the first is an nsIMsgFolder, and the second is a list of all of the nsIMsgDBHdrs
+ * for the synthetic messages in the set inserted into that folder.
+ */
+ get foldersWithMsgHdrs() {
+ let results = this.msgFolders.map(folder => [folder, []]);
+ for (let [iMsg, synMsg] of this.synMessages.entries()) {
+ let folderIndex = this.folderIndices[iMsg];
+ if (folderIndex != null) {
+ let [folder, msgHdrs] = results[folderIndex];
+ msgHdrs.push(
+ folder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId)
+ );
+ }
+ }
+ return results;
+ },
+ /**
+ * Sets the status of the messages to read/unread.
+ *
+ * @param {boolean} aRead - true/false to set messages as read/unread
+ * @param {nsIMsgDBHdr} aMsgHdr - A message header to work on. If not
+ * specified, mark all messages in the current set.
+ */
+ setRead(aRead, aMsgHdr) {
+ let msgHdrs = aMsgHdr ? [aMsgHdr] : this.msgHdrList;
+ for (let msgHdr of msgHdrs) {
+ msgHdr.markRead(aRead);
+ }
+ },
+ /**
+ * Sets the starred status of the messages.
+ *
+ * @param {boolean} aStarred - Starred status.
+ */
+ setStarred(aStarred) {
+ for (let msgHdr of this.msgHdrs()) {
+ msgHdr.markFlagged(aStarred);
+ }
+ },
+ /**
+ * Adds tag to the messages.
+ *
+ * @param {string} aTagName - Tag to add
+ */
+ addTag(aTagName) {
+ for (let [folder, msgHdrs] of this.foldersWithMsgHdrs) {
+ folder.addKeywordsToMessages(msgHdrs, aTagName);
+ }
+ },
+ /**
+ * Removes tag from the messages.
+ *
+ * @param {string} aTagName - Tag to remove
+ */
+ removeTag(aTagName) {
+ for (let [folder, msgHdrs] of this.foldersWithMsgHdrs) {
+ folder.removeKeywordsFromMessages(msgHdrs, aTagName);
+ }
+ },
+ /**
+ * Sets the junk score for the messages to junk/non-junk. It does not
+ * involve the bayesian classifier because we really don't want it
+ * affecting our unit tests! (Unless we were testing the bayesian
+ * classifier. Which I'm conveniently not. Feel free to add a
+ * "setJunkForRealsies" method if you are.)
+ *
+ * @param {boolean} aIsJunk - true/false to set messages to junk/non-junk
+ * @param {nsIMsgDBHdr} aMsgHdr - A message header to work on. If not
+ * specified, mark all messages in the current set.
+ * Generates a msgsJunkStatusChanged nsIMsgFolderListener notification.
+ */
+ setJunk(aIsJunk, aMsgHdr) {
+ let junkscore = aIsJunk ? "100" : "0";
+ let msgHdrs = aMsgHdr ? [aMsgHdr] : this.msgHdrList;
+ for (let msgHdr of msgHdrs) {
+ msgHdr.setStringProperty("junkscore", junkscore);
+ }
+ MailServices.mfn.notifyMsgsJunkStatusChanged(msgHdrs);
+ },
+
+ /**
+ * Slice the message set using the exact Array.prototype.slice semantics
+ * (because we call Array.prototype.slice).
+ */
+ slice(...aArgs) {
+ let slicedMessages = this.synMessages.slice(...aArgs);
+ let slicedIndices = this.folderIndices.slice(...aArgs);
+ let sliced = new SyntheticMessageSet(
+ slicedMessages,
+ this.msgFolders,
+ slicedIndices
+ );
+ if ("glodaMessages" in this && this.glodaMessages) {
+ sliced.glodaMessages = this.glodaMessages.slice(...aArgs);
+ }
+ return sliced;
+ },
+};
+
+/**
+ * Provides mechanisms for creating vaguely interesting, but at least valid,
+ * SyntheticMessage instances.
+ */
+function MessageGenerator() {
+ this._clock = new Date(2000, 1, 1);
+ this._nextNameNumber = 0;
+ this._nextSubjectNumber = 0;
+ this._nextMessageIdNum = 0;
+}
+
+MessageGenerator.prototype = {
+ /**
+ * The maximum number of unique names makeName can produce.
+ */
+ MAX_VALID_NAMES: FIRST_NAMES.length * LAST_NAMES.length,
+ /**
+ * The maximum number of unique e-mail address makeMailAddress can produce.
+ */
+ MAX_VALID_MAIL_ADDRESSES: FIRST_NAMES.length * LAST_NAMES.length,
+ /**
+ * The maximum number of unique subjects makeSubject can produce.
+ */
+ MAX_VALID_SUBJECTS:
+ SUBJECT_ADJECTIVES.length * SUBJECT_NOUNS.length * SUBJECT_SUFFIXES,
+
+ /**
+ * Generate a consistently determined (and reversible) name from a unique
+ * value. Currently up to 26*26 unique names can be generated, which
+ * should be sufficient for testing purposes, but if your code cares, check
+ * against MAX_VALID_NAMES.
+ *
+ * @param {integer} aNameNumber The 'number' of the name you want which must be less
+ * than MAX_VALID_NAMES.
+ * @returns {string} The unique name corresponding to the name number.
+ */
+ makeName(aNameNumber) {
+ let iFirst = aNameNumber % FIRST_NAMES.length;
+ let iLast =
+ (iFirst + Math.floor(aNameNumber / FIRST_NAMES.length)) %
+ LAST_NAMES.length;
+
+ return FIRST_NAMES[iFirst] + " " + LAST_NAMES[iLast];
+ },
+
+ /**
+ * Generate a consistently determined (and reversible) e-mail address from
+ * a unique value; intended to work in parallel with makeName. Currently
+ * up to 26*26 unique addresses can be generated, but if your code cares,
+ * check against MAX_VALID_MAIL_ADDRESSES.
+ *
+ * @param {integer} aNameNumber - The 'number' of the mail address you want
+ * which must be ess than MAX_VALID_MAIL_ADDRESSES.
+ * @returns {string} The unique name corresponding to the name mail address.
+ */
+ makeMailAddress(aNameNumber) {
+ let iFirst = aNameNumber % FIRST_NAMES.length;
+ let iLast =
+ (iFirst + Math.floor(aNameNumber / FIRST_NAMES.length)) %
+ LAST_NAMES.length;
+
+ return (
+ FIRST_NAMES[iFirst].toLowerCase() +
+ "@" +
+ LAST_NAMES[iLast].toLowerCase() +
+ ".invalid"
+ );
+ },
+
+ /**
+ * Generate a pair of name and e-mail address.
+ *
+ * @param {integer} aNameNumber - The optional 'number' of the name and mail
+ * address you want. If you do not provide a value, we will increment an
+ * internal counter to ensure that a new name is allocated and that will not
+ * be re-used. If you use our automatic number once, you must use it
+ * always, unless you don't mind or can ensure no collisions occur between
+ * our number allocation and your uses. If provided, the number must be
+ * less than MAX_VALID_NAMES.
+ * @returns {string[]} A list containing two elements.
+ * The first is a name produced by a call to makeName, and the second an
+ * e-mail address produced by a call to makeMailAddress.
+ * This representation is used by the SyntheticMessage class when dealing
+ * with names and addresses.
+ */
+ makeNameAndAddress(aNameNumber) {
+ if (aNameNumber === undefined) {
+ aNameNumber = this._nextNameNumber++;
+ }
+ return [this.makeName(aNameNumber), this.makeMailAddress(aNameNumber)];
+ },
+
+ /**
+ * Generate and return multiple pairs of names and e-mail addresses. The
+ * names are allocated using the automatic mechanism as documented on
+ * makeNameAndAddress. You should accordingly not allocate / hard code name
+ * numbers on your own.
+ *
+ * @param {integer} aCount - The number of people you want name and address tuples for.
+ * @returns {string[][]} A list of aCount name-and-address tuples.
+ */
+ makeNamesAndAddresses(aCount) {
+ let namesAndAddresses = [];
+ for (let i = 0; i < aCount; i++) {
+ namesAndAddresses.push(this.makeNameAndAddress());
+ }
+ return namesAndAddresses;
+ },
+
+ /**
+ * Generate a consistently determined (and reversible) subject from a unique
+ * value. Up to MAX_VALID_SUBJECTS can be produced.
+ *
+ * @param {integer} aSubjectNumber - The subject number you want generated,
+ * must be less than MAX_VALID_SUBJECTS.
+ * @returns {string} The subject corresponding to the given subject number.
+ */
+ makeSubject(aSubjectNumber) {
+ if (aSubjectNumber === undefined) {
+ aSubjectNumber = this._nextSubjectNumber++;
+ }
+ let iAdjective = aSubjectNumber % SUBJECT_ADJECTIVES.length;
+ let iNoun =
+ (iAdjective + Math.floor(aSubjectNumber / SUBJECT_ADJECTIVES.length)) %
+ SUBJECT_NOUNS.length;
+ let iSuffix =
+ (iNoun +
+ Math.floor(
+ aSubjectNumber / (SUBJECT_ADJECTIVES.length * SUBJECT_NOUNS.length)
+ )) %
+ SUBJECT_SUFFIXES.length;
+ return (
+ SUBJECT_ADJECTIVES[iAdjective] +
+ " " +
+ SUBJECT_NOUNS[iNoun] +
+ " " +
+ SUBJECT_SUFFIXES[iSuffix]
+ );
+ },
+
+ /**
+ * Fabricate a message-id suitable for the given synthetic message. Although
+ * we don't use the message yet, in theory it would let us tailor the
+ * message id to the server that theoretically might be sending it. Or some
+ * such.
+ *
+ * @param {SyntheticMessage} aSynthMessage - The synthetic message you would
+ * like us to make up a message-id for. We don't set the message-id on the
+ * message, that's up to you.
+ * @returns {string} A Message-Id suitable for the given message.
+ */
+ makeMessageId(aSynthMessage) {
+ let msgId = this._nextMessageIdNum + "@made.up.invalid";
+ this._nextMessageIdNum++;
+ return msgId;
+ },
+
+ /**
+ * Generates a valid date which is after all previously issued dates by this
+ * method, ensuring an apparent ordering of time consistent with the order
+ * in which code is executed / messages are generated.
+ * If you need a precise time ordering or precise times, make them up
+ * yourself.
+ *
+ * @returns {Date} - A made-up time in JavaScript Date object form.
+ */
+ makeDate() {
+ let date = this._clock;
+ // advance time by an hour
+ this._clock = new Date(date.valueOf() + 60 * 60 * 1000);
+ return date;
+ },
+
+ /**
+ * Description for makeMessage options parameter.
+ *
+ * @typedef MakeMessageOptions
+ * @property {number} [age] A dictionary with potential attributes 'minutes',
+ * 'hours', 'days', 'weeks' to specify the message be created that far in
+ * the past.
+ * @property {object} [attachments] A list of dictionaries suitable for passing to
+ * syntheticPartLeaf, plus a 'body' attribute that has already been
+ * encoded. Line chopping is on you FOR NOW.
+ * @property {SyntheticPartLeaf} [body] A dictionary suitable for passing to SyntheticPart plus
+ * a 'body' attribute that has already been encoded (if encoding is
+ * required). Line chopping is on you FOR NOW. Alternately, use
+ * bodyPart.
+ * @property {SyntheticPartLeaf} [bodyPart] A SyntheticPart to uses as the body. If you
+ * provide an attachments value, this part will be wrapped in a
+ * multipart/mixed to also hold your attachments. (You can put
+ * attachments in the bodyPart directly if you want and not use
+ * attachments.)
+ * @property {string} [callerData] A value to propagate to the callerData attribute
+ * on the resulting message.
+ * @property {string[][]} [cc] A list of cc recipients (name and address pairs). If
+ * omitted, no cc is generated.
+ * @property {string[][]} [from] The name and value pair this message should be from.
+ * Defaults to the first recipient if this is a reply, otherwise a new
+ * person is synthesized via |makeNameAndAddress|.
+ * @property {string} [inReplyTo] the SyntheticMessage this message should be in
+ * reply-to. If that message was in reply to another message, we will
+ * appropriately compensate for that. If a SyntheticMessageSet is
+ * provided we will use the first message in the set.
+ * @property {boolean} [replyAll] a boolean indicating whether this should be a
+ * reply-to-all or just to the author of the message. (er, to-only, not
+ * cc.)
+ * @property {string} [subject] subject to use; you are responsible for doing any
+ * encoding before passing it in.
+ * @property {string[][]} [to] The list of recipients for this message, defaults to a
+ * set of toCount newly created persons.
+ * @property {number} [toCount=1] the number of people who the message should be to.
+ * @property {object} [clobberHeaders] An object whose contents will overwrite the
+ * contents of the headers object. This should only be used to construct
+ * illegal header values; general usage should use another explicit
+ * mechanism.
+ * @property {boolean} [junk] Should this message be flagged as junk for the benefit
+ * of the MessageInjection helper so that it can know to flag the message
+ * as junk? We have no concept of marking a message as definitely not
+ * junk at this point.
+ * @property {boolean} [read] Should this message be marked as already read?
+ */
+ /**
+ * Create a SyntheticMessage. All arguments are optional, but allow
+ * additional control. With no arguments specified, a new name/address will
+ * be generated that has not been used before, and sent to a new name/address
+ * that has not been used before.
+ *
+ * @param {MakeMessageOptions} aArgs
+ * @returns {SyntheticMessage} a SyntheticMessage fashioned just to your liking.
+ */
+ makeMessage(aArgs) {
+ aArgs = aArgs || {};
+ let msg = new SyntheticMessage();
+
+ if (aArgs.inReplyTo) {
+ // If inReplyTo is a SyntheticMessageSet, just use the first message in
+ // the set because the caller may be using them.
+ let srcMsg = aArgs.inReplyTo.synMessages
+ ? aArgs.inReplyTo.synMessages[0]
+ : aArgs.inReplyTo;
+
+ msg.parent = srcMsg;
+ msg.parent.children.push(msg);
+
+ msg.subject = srcMsg.subject.startsWith("Re: ")
+ ? srcMsg.subject
+ : "Re: " + srcMsg.subject;
+ if (aArgs.replyAll) {
+ msg.to = [srcMsg.from].concat(srcMsg.to.slice(1));
+ } else {
+ msg.to = [srcMsg.from];
+ }
+ msg.from = srcMsg.to[0];
+
+ // we want the <>'s.
+ msg.headers["In-Reply-To"] = srcMsg.headers["Message-Id"];
+ msg.headers.References = (srcMsg.headers.References || []).concat([
+ srcMsg.headers["Message-Id"],
+ ]);
+ } else {
+ msg.parent = null;
+
+ msg.subject = aArgs.subject || this.makeSubject();
+ msg.from = aArgs.from || this.makeNameAndAddress();
+ msg.to = aArgs.to || this.makeNamesAndAddresses(aArgs.toCount || 1);
+ if (aArgs.cc) {
+ msg.cc = aArgs.cc;
+ }
+ }
+
+ msg.children = [];
+ msg.messageId = this.makeMessageId(msg);
+ if (aArgs.age) {
+ let age = aArgs.age;
+ // start from 'now'
+ let ts = new Date().valueOf();
+ if (age.minutes) {
+ ts -= age.minutes * 60 * 1000;
+ }
+ if (age.hours) {
+ ts -= age.hours * 60 * 60 * 1000;
+ }
+ if (age.days) {
+ ts -= age.days * 24 * 60 * 60 * 1000;
+ }
+ if (age.weeks) {
+ ts -= age.weeks * 7 * 24 * 60 * 60 * 1000;
+ }
+ msg.date = new Date(ts);
+ } else {
+ msg.date = this.makeDate();
+ }
+
+ if ("clobberHeaders" in aArgs) {
+ for (let key in aArgs.clobberHeaders) {
+ let value = aArgs.clobberHeaders[key];
+ if (value === null) {
+ delete msg.headers[key];
+ } else {
+ msg.headers[key] = value;
+ }
+ // clobber helper...
+ if (key == "From") {
+ msg._from = ["", ""];
+ }
+ if (key == "To") {
+ msg._to = [["", ""]];
+ }
+ if (key == "Cc") {
+ msg._cc = [["", ""]];
+ }
+ }
+ }
+
+ if ("junk" in aArgs && aArgs.junk) {
+ msg.metaState.junk = true;
+ }
+ if ("read" in aArgs && aArgs.read) {
+ msg.metaState.read = true;
+ }
+
+ let bodyPart;
+ if (aArgs.bodyPart) {
+ bodyPart = aArgs.bodyPart;
+ } else if (aArgs.body) {
+ bodyPart = new SyntheticPartLeaf(aArgs.body.body, aArgs.body);
+ } else {
+ // Different messages should have a chance at different bodies.
+ bodyPart = new SyntheticPartLeaf("Hello " + msg.toName + "!");
+ }
+
+ // if it has any attachments, create a multipart/mixed to be the body and
+ // have it be the parent of the existing body and all the attachments
+ if (aArgs.attachments) {
+ let parts = [bodyPart];
+ for (let attachDesc of aArgs.attachments) {
+ parts.push(new SyntheticPartLeaf(attachDesc.body, attachDesc));
+ }
+ bodyPart = new SyntheticPartMultiMixed(parts);
+ }
+
+ msg.bodyPart = bodyPart;
+
+ msg.callerData = aArgs.callerData;
+
+ return msg;
+ },
+
+ /**
+ * Create an encrypted SMime message. It's just a wrapper around makeMessage,
+ * that sets the right content-type. Use like makeMessage.
+ *
+ * @param {MakeMessageOptions} aOptions
+ * @returns {SyntheticMessage}
+ */
+ makeEncryptedSMimeMessage(aOptions) {
+ if (!aOptions) {
+ aOptions = {};
+ }
+ aOptions.clobberHeaders = {
+ "Content-Transfer-Encoding": "base64",
+ "Content-Disposition": 'attachment; filename="smime.p7m"',
+ };
+ if (!aOptions.body) {
+ aOptions.body = {};
+ }
+ aOptions.body.contentType = 'application/pkcs7-mime; name="smime.p7m"';
+ let msg = this.makeMessage(aOptions);
+ return msg;
+ },
+
+ /**
+ * Create an encrypted OpenPGP message. It's just a wrapper around makeMessage,
+ * that sets the right content-type. Use like makeMessage.
+ *
+ * @param {MakeMessageOptions} aOptions
+ * @returns {SyntheticMessage}
+ */
+ makeEncryptedOpenPGPMessage(aOptions) {
+ if (!aOptions) {
+ aOptions = {};
+ }
+ aOptions.clobberHeaders = {
+ "Content-Transfer-Encoding": "base64",
+ };
+ if (!aOptions.body) {
+ aOptions.body = {};
+ }
+ aOptions.body.contentType =
+ 'multipart/encrypted; protocol="application/pgp-encrypted"';
+ let msg = this.makeMessage(aOptions);
+ return msg;
+ },
+
+ MAKE_MESSAGES_DEFAULTS: {
+ count: 10,
+ },
+ MAKE_MESSAGES_PROPAGATE: [
+ "attachments",
+ "body",
+ "cc",
+ "from",
+ "inReplyTo",
+ "subject",
+ "to",
+ "clobberHeaders",
+ "junk",
+ "read",
+ ],
+ /**
+ * Given a set definition, produce a list of synthetic messages.
+ *
+ * The set definition supports the following attributes:
+ * count: The number of messages to create.
+ * age: As used by makeMessage.
+ * age_incr: Similar to age, but used to increment the values in the age
+ * dictionary (assuming a value of zero if omitted).
+ *
+ * @param {object} aSetDef - Message properties, see MAKE_MESSAGES_PROPAGATE.
+ * @param {integer} [aSetDef.msgsPerThread=1] The number of messages per thread.
+ * If you want to create direct-reply threads, you can pass a value for this
+ * and have it not be one. If you need fancier reply situations,
+ * directly use a scenario or hook us up to support that.
+ *
+ * Also supported are the following attributes as defined by makeMessage:
+ * attachments, body, from, inReplyTo, subject, to, clobberHeaders, junk
+ *
+ * If omitted, the following defaults are used, but don't depend on this as we
+ * can change these at any time:
+ * - count: 10
+ */
+ makeMessages(aSetDef) {
+ let messages = [];
+
+ let args = {};
+ // zero out all the age_incr fields in age (if present)
+ if (aSetDef.age_incr) {
+ args.age = {};
+ for (let unit of Object.keys(aSetDef.age_incr)) {
+ args.age[unit] = 0;
+ }
+ }
+ // copy over the initial values from age (if present)
+ if (aSetDef.age) {
+ args.age = args.age || {};
+ for (let [unit, value] of Object.entries(aSetDef.age)) {
+ args.age[unit] = value;
+ }
+ }
+ // just copy over any attributes found from MAKE_MESSAGES_PROPAGATE
+ for (let propAttrName of this.MAKE_MESSAGES_PROPAGATE) {
+ if (aSetDef[propAttrName]) {
+ args[propAttrName] = aSetDef[propAttrName];
+ }
+ }
+
+ let count = aSetDef.count || this.MAKE_MESSAGES_DEFAULTS.count;
+ let messagsPerThread = aSetDef.msgsPerThread || 1;
+ let lastMessage = null;
+ for (let iMsg = 0; iMsg < count; iMsg++) {
+ // primitive threading support...
+ if (lastMessage && iMsg % messagsPerThread != 0) {
+ args.inReplyTo = lastMessage;
+ } else if (!("inReplyTo" in aSetDef)) {
+ args.inReplyTo = null;
+ }
+ lastMessage = this.makeMessage(args);
+ messages.push(lastMessage);
+
+ if (aSetDef.age_incr) {
+ for (let [unit, delta] of Object.entries(aSetDef.age_incr)) {
+ args.age[unit] += delta;
+ }
+ }
+ }
+
+ return messages;
+ },
+};
+
+/**
+ * Repository of generative message scenarios. Uses the magic bindMethods
+ * function below to allow you to reference methods/attributes without worrying
+ * about how those methods will get the right 'this' pointer if passed as
+ * simply a function argument to someone. So if you do:
+ * foo = messageScenarioFactory.method, followed by foo(...), it will be
+ * equivalent to having simply called messageScenarioFactory.method(...).
+ * (Normally this would not be the case when using JavaScript.)
+ *
+ * @param {MessageGenerator} [aMessageGenerator] The optional message generator we should use.
+ * If you don't pass one, we create our own. You would want to pass one so
+ * that if you also create synthetic messages directly via the message
+ * generator then the two sources can avoid duplicate use of the same
+ * names/addresses/subjects/message-ids.
+ */
+function MessageScenarioFactory(aMessageGenerator) {
+ if (!aMessageGenerator) {
+ aMessageGenerator = new MessageGenerator();
+ }
+ this._msgGen = aMessageGenerator;
+}
+
+MessageScenarioFactory.prototype = {
+ /** Create a chain of direct-reply messages of the given length. */
+ directReply(aNumMessages) {
+ aNumMessages = aNumMessages || 2;
+ let messages = [this._msgGen.makeMessage()];
+ for (let i = 1; i < aNumMessages; i++) {
+ messages.push(this._msgGen.makeMessage({ inReplyTo: messages[i - 1] }));
+ }
+ return messages;
+ },
+
+ /** Two siblings (present), one parent (missing). */
+ siblingsMissingParent() {
+ let missingParent = this._msgGen.makeMessage();
+ let msg1 = this._msgGen.makeMessage({ inReplyTo: missingParent });
+ let msg2 = this._msgGen.makeMessage({ inReplyTo: missingParent });
+ return [msg1, msg2];
+ },
+
+ /** Present parent, missing child, present grand-child. */
+ missingIntermediary() {
+ let msg1 = this._msgGen.makeMessage();
+ let msg2 = this._msgGen.makeMessage({ inReplyTo: msg1 });
+ let msg3 = this._msgGen.makeMessage({ inReplyTo: msg2 });
+ return [msg1, msg3];
+ },
+
+ /**
+ * The root message and all non-leaf nodes have aChildrenPerParent children,
+ * for a total of aHeight layers. (If aHeight is 1, we have just the root;
+ * if aHeight is 2, the root and his aChildrePerParent children.)
+ */
+ fullPyramid(aChildrenPerParent, aHeight) {
+ let msgGen = this._msgGen;
+ let root = msgGen.makeMessage();
+ let messages = [root];
+ function helper(aParent, aRemDepth) {
+ for (let iChild = 0; iChild < aChildrenPerParent; iChild++) {
+ let child = msgGen.makeMessage({ inReplyTo: aParent });
+ messages.push(child);
+ if (aRemDepth) {
+ helper(child, aRemDepth - 1);
+ }
+ }
+ }
+ if (aHeight > 1) {
+ helper(root, aHeight - 2);
+ }
+ return messages;
+ },
+};
+
+/**
+ * Decorate the given object's methods will python-style method binding. We
+ * create a getter that returns a method that wraps the call, providing the
+ * actual method with the 'this' of the object that was 'this' when the getter
+ * was called.
+ * Note that we don't follow the prototype chain; we only process the object you
+ * immediately pass to us. This does not pose a problem for the 'this' magic
+ * because we are using a getter and 'this' in js always refers to the object
+ * in question (never any part of its prototype chain). As such, you probably
+ * want to invoke us on your prototype object(s).
+ *
+ * @param {object} aObj - The object on whom we want to perform magic binding.
+ * This should probably be your prototype object.
+ */
+function bindMethods(aObj) {
+ for (let [name, ubfunc] of Object.entries(aObj)) {
+ // the variable binding needs to get captured...
+ let realFunc = ubfunc;
+ delete aObj[name];
+ Object.defineProperty(aObj, name, {
+ get() {
+ return realFunc.bind(this);
+ },
+ });
+ }
+}
+
+bindMethods(MessageScenarioFactory.prototype);