diff options
Diffstat (limited to 'comm/mailnews/test/resources/MessageGenerator.jsm')
-rw-r--r-- | comm/mailnews/test/resources/MessageGenerator.jsm | 1651 |
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); |