/* 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/. */

for (let stack = Components.stack; stack; stack = stack.caller) {
  if (
    stack.filename.split("/").at(-1).startsWith("browser_") &&
    !stack.filename.includes("comm/mail/test/browser/")
  ) {
    // Importing this file has unintended consequences. It should not be used
    // outside of mail/test/browser and, ideally, not be used in any new tests.
    throw new Error(
      "Do not import MessageInjection.sys.mjs outside of mail/test/browser."
    );
  }
}

import { mailTestUtils } from "resource://testing-common/mailnews/MailTestUtils.sys.mjs";

import { MailServices } from "resource:///modules/MailServices.sys.mjs";
import { MailUtils } from "resource:///modules/MailUtils.sys.mjs";
import { PromiseTestUtils } from "resource://testing-common/mailnews/PromiseTestUtils.sys.mjs";
import { SyntheticMessageSet } from "resource://testing-common/mailnews/MessageGenerator.sys.mjs";

import { VirtualFolderHelper } from "resource:///modules/VirtualFolderWrapper.sys.mjs";
import { ImapMessage } from "resource://testing-common/mailnews/Imapd.sys.mjs";

import {
  IMAPPump,
  setupIMAPPump,
} from "resource://testing-common/mailnews/IMAPpump.sys.mjs";

const SEARCH_TERM_MAP_HELPER = {
  subject: Ci.nsMsgSearchAttrib.Subject,
  body: Ci.nsMsgSearchAttrib.Body,
  from: Ci.nsMsgSearchAttrib.Sender,
  to: Ci.nsMsgSearchAttrib.To,
  cc: Ci.nsMsgSearchAttrib.CC,
  recipient: Ci.nsMsgSearchAttrib.ToOrCC,
  involves: Ci.nsMsgSearchAttrib.AllAddresses,
  age: Ci.nsMsgSearchAttrib.AgeInDays,
  tags: Ci.nsMsgSearchAttrib.Keywords,
  // If a test uses a custom search term, they must register that term
  //  with the id "mailnews@mozilla.org#test"
  custom: Ci.nsMsgSearchAttrib.Custom,
};

/**
 * Handling for Messages in Folders. Usage of either `local` or `imap`.
 *
 * Beware:
 * Currently only one active instance of MessageInjection is supported due
 * to a dependency on retrieving an account in the constructor.
 */
export class MessageInjection {
  /**
   * MessageInjectionSetup
   */
  _mis = {
    _nextUniqueFolderId: 0,

    injectionConfig: {
      mode: "none",
    },
    listeners: [],
    notifyListeners(handlerName, args) {
      for (const listener of this.listeners) {
        if (handlerName in listener) {
          listener[handlerName].apply(listener, args);
        }
      }
    },

    /**
     * The nsIMsgIncomingServer
     */
    incomingServer: null,

    /**
     * The incoming server's (synthetic) root message folder.
     */
    rootFolder: null,

    /**
     * The nsIMsgFolder that is the inbox.
     */
    inboxFolder: null,

    /**
     * Fakeserver daemon, if applicable.
     */
    daemon: null,
    /**
     * Fakeserver server instance, if applicable.
     */
    server: null,
  };
  /**
   * Creates an environment for tests.
   * Usage either with "local" or "imap".
   * An Inbox folder is created. Retrieve it via `getInboxFolder`
   *
   * IMAP:
   *  Starts an IMAP Server for you
   *
   * @param {object} injectionConfig
   * @param {"local"|"imap"} injectionConfig.mode mode One of "local", "imap".
   * @param {boolean} [injectionConfig.offline] Should the folder be marked offline (and
   *     fully downloaded)?  Only relevant for IMAP.
   * @param {MessageGenerator} [msgGen] The MessageGenerator which generates the new
   *     SyntheticMessages. We do not create our own because we would lose track of
   *     messages created from another MessageGenerator.
   *     It's optional as it is used only for a subset of methods.
   */
  constructor(injectionConfig, msgGen) {
    // Set the injection Mode.
    this._mis.injectionConfig = injectionConfig;

    // Disable new mail notifications.
    Services.prefs.setBoolPref("mail.biff.play_sound", false);
    Services.prefs.setBoolPref("mail.biff.show_alert", false);
    Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
    Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);

    // Set msgGen if given.
    if (msgGen) {
      this.msgGen = msgGen;
    }

    // we need to pull in the notification service so we get events?
    MailServices.mfn;

    if (this._mis.injectionConfig.mode == "local") {
      // This does createIncomingServer() and createAccount(), sets the server as
      //  the account's server, then sets the server.
      let localAccount;
      try {
        localAccount = MailServices.accounts.findAccountForServer(
          MailServices.accounts.localFoldersServer
        );
      } catch (ex) {
        localAccount = MailServices.accounts.createLocalMailAccount();
      }

      // We need an identity or we get angry warnings.
      let identity = localAccount.defaultIdentity;
      if (!identity) {
        identity = MailServices.accounts.createIdentity();
        // We need an email to protect against random code assuming it exists and
        // throwing exceptions.
        identity.email = "sender@nul.invalid";
        localAccount.addIdentity(identity);
        localAccount.defaultIdentity = identity;
      }

      this._mis.incomingServer = MailServices.accounts.localFoldersServer;
      // Note: Inbox is not created automatically when there is no deferred server,
      // so we need to create it.
      this._mis.rootFolder = this._mis.incomingServer.rootMsgFolder;
      this._mis.rootFolder.createSubfolder("Inbox", null);
      this._mis.inboxFolder = this._mis.rootFolder.getChildNamed("Inbox");
      // a local inbox should have a Mail flag!
      this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
      this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Inbox);
      this._mis.notifyListeners("onRealFolderCreated", [this._mis.inboxFolder]);

      // Force an initialization of the Inbox folder database.
      this._mis.inboxFolder.name;
    } else if (this._mis.injectionConfig.mode == "imap") {
      // Disable autosync in favor of our explicitly forcing downloads of all
      //  messages in a folder.  This is being done speculatively because when we
      //  didn't do this we got tripped up by the semaphore being in use and
      //  concern over inability to hang a listener off of the completion of the
      //  download.  (Although I'm sure there are various ways we could do it.)
      Services.prefs.setBoolPref(
        "mail.server.default.autosync_offline_stores",
        false
      );
      // Set the offline property based on the configured setting.  This will
      //  affect newly created folders.
      Services.prefs.setBoolPref(
        "mail.server.default.offline_download",
        this._mis.injectionConfig.offline
      );

      // set up IMAP fakeserver and incoming server
      setupIMAPPump("");
      this._mis.daemon = IMAPPump.daemon;
      this._mis.server = IMAPPump.server;
      this._mis.incomingServer = IMAPPump.incomingServer;
      // this.#mis.server._debug = 3;

      // do not log transactions; it's just a memory leak to us
      this._mis.server._logTransactions = false;

      // We need an identity so that updateFolder doesn't fail
      const localAccount = MailServices.accounts.defaultAccount;
      // We need an email to protect against random code assuming it exists and
      // throwing exceptions.
      const identity = localAccount.defaultIdentity;
      identity.email = "sender@nul.invalid";

      // The server doesn't support more than one connection
      Services.prefs.setIntPref(
        "mail.server.server1.max_cached_connections",
        1
      );
      // We aren't interested in downloading messages automatically
      Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false);

      this._mis.rootFolder = this._mis.incomingServer.rootMsgFolder;

      this._mis.inboxFolder = this._mis.rootFolder.getChildNamed("Inbox");
      // make sure the inbox's offline state is correct. (may be excessive now
      //  that we set the pref above?)
      if (this._mis.injectionConfig.offline) {
        this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
      } else {
        this._mis.inboxFolder.clearFlag(Ci.nsMsgFolderFlags.Offline);
      }
      this._mis.notifyListeners("onRealFolderCreated", [this._mis.inboxFolder]);

      this._mis.handleUriToRealFolder = {};
      this._mis.handleUriToFakeFolder = {};
      this._mis.realUriToFakeFolder = {};
      this._mis.realUriToFakeFolder[this._mis.inboxFolder.URI] =
        this._mis.daemon.getMailbox("INBOX");
    } else {
      throw new Error(
        "Illegal injection config option: " + this._mis.injectionConfig.mode
      );
    }

    this._mis.junkHandle = null;
    this._mis.junkFolder = null;

    this._mis.trashHandle = null;
    this._mis.trashFolder = null;
  }

  /**
   * @returns {nsIMsgFolder}
   */
  getInboxFolder() {
    return this._mis.inboxFolder;
  }
  /**
   * @returns {boolean}
   */
  messageInjectionIsLocal() {
    return this._mis.injectionConfig.mode == "local";
  }
  /**
   * Call this method to finish the use of MessageInjection.
   *  Stops the IMAP server (if used) and stops internal functions.
   */
  teardownMessageInjection() {
    if (this._mis.injectionConfig.mode == "imap") {
      this._mis.incomingServer.closeCachedConnections();

      // No more tests, let everything finish.
      // (This spins its own event loop...)
      this._mis.server.stop();
    }

    // Clean out this.#mis; we don't just null the global because it's conceivable we
    //  might still have some closures floating about.
    for (const key in this._mis) {
      delete this._mis[key];
    }
  }
  /**
   * Register a listener to be notified when interesting things happen involving
   *  calls made to the message injection API.
   *
   * @param {object} listener
   * @param {Function} listener.onVirtualFolderCreated Called when a virtual
   *   folder is created using |makeVirtualFolder|. Takes a nsIMsgFolder
   *   that defines the virtual folder as argument.
   */
  registerMessageInjectionListener(listener) {
    this._mis.listeners.push(listener);
  }
  /**
   * Create and return an empty folder.  If you want to delete this folder
   *  you must call |deleteFolder| to kill it!  If you want to rename it, you
   *  must implement a method called renameFolder and then call it.
   *
   * @param {string} [folderName] A folder name with no support for hierarchy at this
   *     time.  A name of the form "gabba#" will be autogenerated if you do not
   *     provide one.
   * @param {nsMsgFolderFlags[]} [specialFlags] A list of nsMsgFolderFlags bits to set.
   * @returns {nsIMsgFolder|string} In local mode a nsIMsgFolder is returned.
   *     In imap mode a folder URI is returned.
   */
  async makeEmptyFolder(folderName, specialFlags) {
    if (folderName == null) {
      folderName = "gabba" + this._mis._nextUniqueFolderId++;
    }
    let testFolder;

    if (this._mis.injectionConfig.mode == "local") {
      const localRoot = this._mis.rootFolder.QueryInterface(
        Ci.nsIMsgLocalMailFolder
      );
      testFolder = localRoot.createLocalSubfolder(folderName);
      // it seems dumb that we have to set this.
      testFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
      if (specialFlags) {
        for (const flag of specialFlags) {
          testFolder.setFlag(flag);
        }
      }
      this._mis.notifyListeners("onRealFolderCreated", [testFolder]);
    } else if (this._mis.injectionConfig.mode == "imap") {
      // Circumvent this scoping.
      const mis = this._mis;
      const promiseUrlListener = new PromiseTestUtils.PromiseUrlListener({
        OnStopRunningUrl: () => {
          // get the newly created nsIMsgFolder folder
          const msgFolder = mis.rootFolder.getChildNamed(folderName);

          // XXX there is a bug that causes folders to be reported as ImapPublic
          //  when there is no namespace support by the IMAP server.  This is
          //  a temporary workaround.
          msgFolder.clearFlag(Ci.nsMsgFolderFlags.ImapPublic);
          msgFolder.setFlag(Ci.nsMsgFolderFlags.ImapPersonal);

          if (specialFlags) {
            for (const flag of specialFlags) {
              msgFolder.setFlag(flag);
            }
          }

          // get a reference to the fake server folder
          const fakeFolder = this._mis.daemon.getMailbox(folderName);
          // establish the mapping
          mis.handleUriToRealFolder[testFolder] = msgFolder;
          mis.handleUriToFakeFolder[testFolder] = fakeFolder;
          mis.realUriToFakeFolder[msgFolder.URI] = fakeFolder;

          // notify listeners
          mis.notifyListeners("onRealFolderCreated", [msgFolder]);
        },
      });

      testFolder = this._mis.rootFolder.URI + "/" + folderName;

      // Tell the IMAP service to create the folder, adding a listener that
      //  hooks up the 'handle' URI -> actual folder mapping.
      MailServices.imap.createFolder(
        this._mis.rootFolder,
        folderName,
        promiseUrlListener
      );
      await promiseUrlListener.promise;
    }

    return testFolder;
  }
  /**
   * Small helper for moving folder.
   *
   * @param {nsIMsgFolder} source
   * @param {nsIMsgFolder} target
   */
  static async moveFolder(source, target) {
    // we're doing a true move
    await new Promise((resolve, reject) => {
      MailServices.copy.copyFolder(
        MessageInjection.get_nsIMsgFolder(source),
        MessageInjection.get_nsIMsgFolder(target),
        true,
        /** @implements {nsIMsgCopyServiceListener} */
        {
          onStartCopy() {},
          onProgress() {},
          setMessageKey() {},
          getMessageId() {
            return null;
          },
          onStopCopy(status) {
            if (Components.isSuccessCode(status)) {
              resolve();
            } else {
              reject();
            }
          },
        },
        null
      );
    });
  }
  /**
   *
   * Get/create the junk folder handle.  Use getRealInjectionFolder if you
   *  need the underlying nsIFolder.
   *
   * @returns {nsIMsgFolder}
   */
  async getJunkFolder() {
    if (!this._mis.junkHandle) {
      this._mis.junkHandle = await this.makeEmptyFolder("Junk", [
        Ci.nsMsgFolderFlags.Junk,
      ]);
    }

    return this._mis.junkHandle;
  }
  /**
   * Get/create the trash folder handle.  Use getRealInjectionFolder if you
   *  need the underlying nsIMsgFolder.
   *
   * @returns {nsIMsgFolder|string}
   */
  async getTrashFolder() {
    if (!this._mis.trashHandle) {
      // the folder may have been created and already known...
      this._mis.trashFolder = this._mis.rootFolder.getFolderWithFlags(
        Ci.nsMsgFolderFlags.Trash
      );
      if (this._mis.trashFolder) {
        this._mis.trashHandle = this._mis.rootFolder.URI + "/Trash";
        const fakeFolder = this._mis.daemon.getMailbox("Trash");
        this._mis.handleUriToRealFolder[this._mis.trashHandle] =
          this._mis.trashFolder;
        this._mis.handleUriToFakeFolder[this._mis.trashHandle] = fakeFolder;
        this._mis.realUriToFakeFolder[this._mis.trashFolder.URI] = fakeFolder;
      } else {
        this._mis.trashHandle = await this.makeEmptyFolder("Trash", [
          Ci.nsMsgFolderFlags.Trash,
        ]);
      }
    }

    return this._mis.trashHandle;
  }
  /**
   * Create and return a virtual folder.
   *
   * @param {nsIMsgFolder[]} folders The real folders this virtual folder should draw from.
   * @param {SEARCH_TERM_MAP_HELPER} searchDef The search definition to use
   *     to build the list of search terms that populate this virtual folder.
   *     Keys should be stuff from SEARCH_TERM_MAP_HELPER and values should be
   *     strings to search for within those attribute things.
   * @param {boolean} [booleanAnd] Should the search terms be and-ed together.
   *     Defaults to false.
   * @param {string} [folderName] Name to use.
   * @returns {nsIMsgFolder|string} In local usage returns a nsIMsgFolder
   *     in imap usage returns a Folder URI.
   */
  makeVirtualFolder(folders, searchDef, booleanAnd, folderName) {
    const name = folderName
      ? folderName
      : "virt" + this._mis._nextUniqueFolderId++;

    const terms = [];
    const termCreator = Cc[
      "@mozilla.org/messenger/searchSession;1"
    ].createInstance(Ci.nsIMsgSearchSession);
    for (const key in searchDef) {
      const val = searchDef[key];
      const term = termCreator.createTerm();
      const value = term.value;
      value.str = val;
      term.value = value;
      term.attrib = SEARCH_TERM_MAP_HELPER[key];
      if (term.attrib == Ci.nsMsgSearchAttrib.Custom) {
        term.customId = "mailnews@mozilla.org#test";
      }
      term.op = Ci.nsMsgSearchOp.Contains;
      term.booleanAnd = Boolean(booleanAnd);
      terms.push(term);
    }
    // create an ALL case if we didn't add any terms
    if (terms.length == 0) {
      const term = termCreator.createTerm();
      term.matchAll = true;
      terms.push(term);
    }

    const wrapped = VirtualFolderHelper.createNewVirtualFolder(
      name,
      this._mis.rootFolder,
      folders,
      terms,
      /* online */ false
    );
    this._mis.notifyListeners("onVirtualFolderCreated", [
      wrapped.virtualFolder,
    ]);
    return wrapped.virtualFolder;
  }
  /**
   * Mark the folder as offline and force all of its messages to be downloaded.
   *  This is an asynchronous operation that will call resolve once the
   *  download is completed.
   *
   * @param {string} folderHandle Folder URI.
   */
  async makeFolderAndContentsOffline(folderHandle) {
    if (this._mis.injectionConfig.mode != "imap") {
      return;
    }

    const msgFolder = this.getRealInjectionFolder(folderHandle);
    msgFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
    const promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
    msgFolder.downloadAllForOffline(promiseUrlListener, null);
    await promiseUrlListener.promise;
  }

  /**
   * Create multiple new local folders, populating them with messages according to
   *  the set definitions provided.  Differs from makeFolderWithSets by taking
   *  the number of folders to create and return the list of created folders as
   *  the first element in the returned list.  This method is simple enough that
   *  the limited code duplication is deemed acceptable in support of readability.
   *
   * @param {number} folderCount
   * @param {MakeMessageOptions[]} synSetDefs A synthetic set
   *     definition, as appropriate to pass to makeNewSetsInFolders.
   * @returns {Promise<object[]>} A Promise with a list whose first element are
   *   the nsIMsgFolders created and whose subsequent items are the
   *   SyntheticMessageSets used to populate the folder (as returned by
   *   makeNewSetsInFolders). So nsIMsgFolder[], ...SyntheticMessageSet.
   *
   *  Please note that the folders are either nsIMsgFolder, or folder
   *     URIs, depending on whether we're in local injection mode, or on IMAP. This
   *     should be transparent to you, unless you start trying to inject messages
   *     into a folder that hasn't been created by makeFoldersWithSets. See
   *     test_folder_deletion_nested in base_index_messages.js for an example of
   *     such pain.
   */
  async makeFoldersWithSets(folderCount, synSetDefs) {
    const msgFolders = [];
    for (let i = 0; i < folderCount; i++) {
      msgFolders.push(await this.makeEmptyFolder());
    }
    let results = await this.makeNewSetsInFolders(msgFolders, synSetDefs);
    // results may be referenced by addSetsToFolders in an async fashion, so
    //  don't change it.
    results = results.concat();
    results.unshift(msgFolders);
    return results;
  }
  /**
   * Given one or more existing folders, create new message sets and
   *   add them to the folders using.
   *
   * @param {nsIMsgFolder[]} msgFolders A list of nsIMsgFolder.
   *     The synthetic messages will be added to the folder(s).
   * @param {MakeMessageOptions[]} synSetDefs A list of set definition objects as
   *     defined by MessageGenerator.makeMessages.
   * @param {boolean} [doNotForceUpdate=false] By default we force an updateFolder on IMAP
   *     folders to ensure Thunderbird knows about the newly injected messages.
   *     If you are testing Thunderbird's use of updateFolder itself, you will
   *     not want this and so will want to pass true for this argument.
   * @returns {SyntheticMessageSet[]} A Promise with a list of SyntheticMessageSet objects,
   *     each corresponding to the entry in synSetDefs (or implied if an integer was passed).
   */
  async makeNewSetsInFolders(msgFolders, synSetDefs, doNotForceUpdate) {
    // - create the synthetic message sets
    const messageSets = [];
    for (const synSetDef of synSetDefs) {
      // Using the getter of the MessageGenerator for error handling.
      const messages = this.messageGenerator.makeMessages(synSetDef);
      messageSets.push(new SyntheticMessageSet(messages));
    }

    // - add the messages to the folders (interleaving them)
    await this.addSetsToFolders(msgFolders, messageSets, doNotForceUpdate);

    return messageSets;
  }
  /**
   * Spreads the messages in messageSets across the folders in msgFolders.  Each
   *  message set is spread in a round-robin fashion across all folders.  At the
   *  same time, each message-sets insertion is interleaved with the other message
   *  sets.  This distributes message across multiple folders for useful
   *  cross-folder threading testing (via the round robin) while also hopefully
   *  avoiding making things pathologically easy for the code under test (by way
   *  of the interleaving.)
   *
   * For example, given the following 2 input message sets:
   *  message set 'lower': [a b c d e f]
   *  message set 'upper': [A B C D E F G H]
   *
   * across 2 folders:
   *  folder 1: [a A c C e E G]
   *  folder 2: [b B d D f F H]
   * across 3 folders:
   *  folder 1: [a A d D G]
   *  folder 2: [b B e E H]
   *  folder 3: [c C f F]
   *
   * @param {nsIMsgFolder[]} msgFolders
   *     An nsIMsgFolder to add the message sets to or a list of them.
   * @param {SyntheticMessageSet[]} messageSets A list of SyntheticMessageSets.
   * @param {boolean} [doNotForceUpdate=false] By default we force an updateFolder on IMAP
   *     folders to ensure Thunderbird knows about the newly injected messages.
   *     If you are testing Thunderbird's use of updateFolder itself, you will
   *     not want this and so will want to pass true for this argument.
   */
  async addSetsToFolders(msgFolders, messageSets, doNotForceUpdate) {
    let iterFolders;

    this._mis.notifyListeners("onInjectingMessages", []);

    // -- Pre-loop
    if (this._mis.injectionConfig.mode == "local") {
      for (const folder of msgFolders) {
        if (!(folder instanceof Ci.nsIMsgLocalMailFolder)) {
          throw new Error("All folders in msgFolders must be local folders!");
        }
      }
    } else if (this._mis.injectionConfig.mode == "imap") {
      // no protection is possible because of our dependency on promises,
      //  although we could check that the fake URL is one we handed out.
    } else {
      throw new Error("Message injection is not configured!");
    }

    if (this._mis.injectionConfig.mode == "local") {
      // Note: in order to cut down on excessive fsync()s, we do a two-pass
      //  approach.  In the first pass we just allocate messages to the folder
      //  we are going to insert them into.  In the second pass we insert the
      //  messages into folders in batches and perform any mutations.
      const folderBatches = msgFolders.map(folder => {
        return { folder, messages: [] };
      });
      iterFolders = this._looperator([...folderBatches.keys()]);
      let iPerSet = 0,
        folderNext = iterFolders.next();

      // - allocate messages to folders
      // loop, incrementing our subscript until all message sets are out of messages
      let didSomething;
      do {
        didSomething = false;
        // for each message set, if it is not out of messages, add the message
        for (const messageSet of messageSets) {
          if (iPerSet < messageSet.synMessages.length) {
            const synMsg = messageSet._trackMessageAddition(
              folderBatches[folderNext.value].folder,
              iPerSet
            );
            folderBatches[folderNext.value].messages.push({
              messageSet,
              synMsg,
              index: iPerSet,
            });
            didSomething = true;
          }
        }
        iPerSet++;
        folderNext = iterFolders.next();
      } while (didSomething);

      // - inject messages
      for (const folderBatch of folderBatches) {
        // it is conceivable some folders might not get any messages, skip them.
        if (!folderBatch.messages.length) {
          continue;
        }

        const folder = folderBatch.folder;
        folder.gettingNewMessages = true;
        const messageStrings = folderBatch.messages.map(message =>
          message.synMsg.toMessageString()
        );
        folder.addMessageBatch(messageStrings);

        for (const message of folderBatch.messages) {
          const synMsgState = message.synMsg.metaState;
          // If we need to mark the message as junk grab the header and do so.
          if (synMsgState.junk) {
            message.messageSet.setJunk(
              true,
              message.messageSet.getMsgHdr(message.index)
            );
          }
          if (synMsgState.read) {
            // XXX this will generate an event; I'm not sure if we should be
            //  trying to avoid that or not.  This case is really only added
            //  for IMAP where this makes more sense.
            message.messageSet.setRead(
              true,
              message.messageSet.getMsgHdr(message.index)
            );
          }
        }
        if (folderBatch.messages.length) {
          const lastMRUTime = Math.floor(
            Number(folderBatch.messages[0].synMsg.date) / 1000
          );
          folder.setStringProperty("MRUTime", lastMRUTime);
        }
        folder.gettingNewMessages = false;
        folder.hasNewMessages = true;
        folder.setNumNewMessages(
          folder.getNumNewMessages(false) + messageStrings.length
        );
        folder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
      }

      // make sure that junk filtering gets a turn
      // XXX we probably need to be doing more in terms of filters here,
      //  although since filters really want to be run on the inbox, there
      //  are separate potential semantic issues involved.
      for (const folder of msgFolders) {
        folder.callFilterPlugins(null);
      }

      if (doNotForceUpdate) {
        return;
      }
      for (const folder of msgFolders) {
        await new Promise(resolve => {
          mailTestUtils.updateFolderAndNotify(folder, resolve);
        });
      }
    } else if (this._mis.injectionConfig.mode == "imap") {
      iterFolders = this._looperator(msgFolders);
      // we need to call updateFolder on all the folders, not just the first
      //  one...
      let iPerSet = 0,
        folder = iterFolders.next();
      let didSomething;
      do {
        didSomething = false;
        for (const messageSet of messageSets) {
          if (iPerSet < messageSet.synMessages.length) {
            didSomething = true;

            const realFolder = this._mis.handleUriToRealFolder[folder.value];
            const fakeFolder = this._mis.handleUriToFakeFolder[folder.value];
            const synMsg = messageSet._trackMessageAddition(
              realFolder,
              iPerSet
            );
            const msgURI = Services.io.newURI(
              "data:text/plain;base64," + btoa(synMsg.toMessageString())
            );
            const imapMsg = new ImapMessage(
              msgURI.spec,
              fakeFolder.uidnext++,
              []
            );
            // If the message's meta-state indicates it is junk, set that flag.
            // There is also a NotJunk flag, but we're not playing with that
            //  right now; as long as nothing is ever marked as junk, the junk
            //  classifier won't run, so it's moot for now.
            if (synMsg.metaState.junk) {
              imapMsg.setFlag("Junk");
            }
            if (synMsg.metaState.read) {
              imapMsg.setFlag("\\Seen");
            }
            fakeFolder.addMessage(imapMsg);
          }
        }
        iPerSet++;
        folder = iterFolders.next();
      } while (didSomething);

      // We have nothing more to do if we aren't support to force the update.
      if (doNotForceUpdate) {
        return;
      }

      for (let iFolder = 0; iFolder < msgFolders.length; iFolder++) {
        const realFolder = this._mis.handleUriToRealFolder[msgFolders[iFolder]];
        await new Promise(resolve => {
          mailTestUtils.updateFolderAndNotify(realFolder, resolve);
        });

        // compel download of the messages if appropriate
        if (realFolder.flags & Ci.nsMsgFolderFlags.Offline) {
          const promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
          realFolder.downloadAllForOffline(promiseUrlListener, null);
          await promiseUrlListener.promise;
        }
      }
    }
  }
  /**
   * Return the nsIMsgFolder associated with a folder handle.  If the folder has
   *  been created since the last injection and you are using IMAP, you may need
   *  to first resolve the Promises for us to be able to provide
   *  you with a result.
   *
   * @param {nsIMsgFolder|string} folderHandle nsIMsgFolder or folder URI.
   * @returns {nsIMsgFolder}
   */
  getRealInjectionFolder(folderHandle) {
    if (this._mis.injectionConfig.mode == "imap") {
      return this._mis.handleUriToRealFolder[folderHandle];
    }
    return folderHandle;
  }
  /**
   * Move messages in the given set to the destination folder.
   *
   * For IMAP moves we force an update of the source folder and then the
   *  destination folder.  This ensures that any (pseudo-)offline operations in
   *  the source folder have had a chance to run and that we have seen the changes
   *  in the target folder.
   * We additionally cause all of the message bodies to be downloaded in the
   *  target folder if the folder has the Offline flag set.
   *
   * @param {SyntheticMessageSet} synMessageSet The messages to move.
   * @param {nsIMsgFolder|string} destFolder The target folder or target folder URI.
   * @param {boolean} [allowUndo=false] Should we generate undo operations and, as a
   *     side-effect, offline operations?  (The code uses undo operations as
   *     a proxy-indicator for it coming from the UI and therefore performing
   *     pseudo-offline operations instead of trying to do things online.)
   */
  async moveMessages(synMessageSet, destFolder, allowUndo) {
    const realDestFolder = this.getRealInjectionFolder(destFolder);

    for (const [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
      // In the IMAP case tell listeners we are moving messages without
      //  destination headers.
      if (!this.messageInjectionIsLocal()) {
        this._mis.notifyListeners("onMovingMessagesWithoutDestHeaders", [
          realDestFolder,
        ]);
      }
      const promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
      MailServices.copy.copyMessages(
        folder,
        msgs,
        realDestFolder,
        /* move */ true,
        promiseCopyListener,
        null,
        Boolean(allowUndo)
      );
      await promiseCopyListener.promise;
      // update the synthetic message set's folder entry...
      synMessageSet._folderSwap(folder, realDestFolder);

      // IMAP special case per function doc...
      if (!this.messageInjectionIsLocal()) {
        // update the source folder to force it to issue the move
        await new Promise(resolve => {
          mailTestUtils.updateFolderAndNotify(folder, resolve);
        });

        // update the dest folder to see the new header.
        await new Promise(resolve => {
          mailTestUtils.updateFolderAndNotify(realDestFolder, resolve);
        });

        // compel download of messages in dest folder if appropriate
        if (realDestFolder.flags & Ci.nsMsgFolderFlags.Offline) {
          const promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
          realDestFolder.downloadAllForOffline(promiseUrlListener, null);
          await promiseUrlListener.promise;
        }
      }
    }
  }
  /**
   * Move the messages to the trash; do not use this on messages that are already
   *  in the trash, we are not clever enough for that.
   *
   * @param {SyntheticMessageSet} synMessageSet The set of messages to trash.
   *     The messages do not all have to be in the same folder,
   *     but we have to trash them folder by folder if they are not.
   */
  async trashMessages(synMessageSet) {
    for (const [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
      // In the IMAP case tell listeners we are moving messages without
      //  destination headers, since that's what trashing amounts to.
      if (!this.messageInjectionIsLocal()) {
        this._mis.notifyListeners("onMovingMessagesWithoutDestHeaders", []);
      }
      const promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
      folder.deleteMessages(
        msgs,
        null,
        false,
        true,
        promiseCopyListener,
        /* do not allow undo, currently leaks */ false
      );
      await promiseCopyListener.promise;

      // just like the move case we need to force updateFolder calls for IMAP
      if (!this.messageInjectionIsLocal()) {
        // update the source folder to force it to issue the move
        await new Promise(resolve => {
          mailTestUtils.updateFolderAndNotify(folder, resolve);
        });

        // trash folder may not have existed at startup but the deletion
        //  will have created it.
        const trashFolder = this.getRealInjectionFolder(
          await this.getTrashFolder()
        );

        // update the dest folder to see the new header.
        await new Promise(resolve => {
          mailTestUtils.updateFolderAndNotify(trashFolder, resolve);
        });

        // compel download of messages in dest folder if appropriate
        if (trashFolder.flags & Ci.nsMsgFolderFlags.Offline) {
          const promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
          trashFolder.downloadAllForOffline(promiseUrlListener, null);
          await promiseUrlListener.promise;
        }
      }
    }
  }
  /**
   * Delete all of the messages in a SyntheticMessageSet like the user performed a
   *  shift-delete (or if the messages were already in the trash).
   *
   * @param {SyntheticMessageSet} synMessageSet The set of messages to delete.
   *     The messages do not all have to be in the same folder, but we have to
   *     delete them folder by folder if they are not.
   */
  static async deleteMessages(synMessageSet) {
    for (const [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
      const promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
      folder.deleteMessages(
        msgs,
        null,
        /* delete storage */ true,
        /* is move? */ false,
        promiseCopyListener,
        /* do not allow undo, currently leaks */ false
      );
      await promiseCopyListener.promise;
    }
  }
  /**
   * Empty the trash.
   */
  async emptyTrash() {
    const trashHandle = await this.getTrashFolder();
    const trashFolder = this.getRealInjectionFolder(trashHandle);
    const promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
    trashFolder.emptyTrash(promiseUrlListener);
    await promiseUrlListener.promise;
  }
  /**
   * Delete the given folder, removing the storage.  We do not move it to the
   *  trash.
   */
  deleteFolder(folder) {
    const realFolder = this.getRealInjectionFolder(folder);
    realFolder.parent.propagateDelete(realFolder, true);
  }

  /**
   * @param {nsIMsgFolder} folder
   * @returns {nsIMsgFolder}
   */
  static get_nsIMsgFolder(folder) {
    if (!(folder instanceof Ci.nsIMsgFolder)) {
      return MailUtils.getOrCreateFolder(folder);
    }
    return folder;
  }
  /**
   * An iterator that generates an infinite sequence of its argument.  So
   *  _looperator(1, 2, 3) will generate the iteration stream: [1, 2, 3, 1, 2, 3,
   *  1, 2, 3, ...].
   */
  *_looperator(list) {
    if (list.length == 0) {
      throw new Error("list must have at least one item!");
    }

    let i = 0;
    const length = list.length;
    while (true) {
      yield list[i];
      i = (i + 1) % length;
    }
  }

  get messageGenerator() {
    if (this.msgGen === undefined) {
      throw new Error(
        "MessageInjection.sys.mjs needs a MessageGenerator for new messages. " +
          "The MessageGenerator helps you with threaded messages. If you use " +
          "two different MessageGenerators the behaviour with threads are complicated."
      );
    }
    return this.msgGen;
  }
  /**
   * @param {MessageGenerator} msgGen The MessageGenerator which generates the new
   *     SyntheticMessages. We do not create our own because we would lose track of
   *     messages created from another MessageGenerator.
   */
  set messageGenerator(msgGen) {
    this.msgGen = msgGen;
  }
}
