summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/imap/src/ImapChannel.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/imap/src/ImapChannel.jsm')
-rw-r--r--comm/mailnews/imap/src/ImapChannel.jsm318
1 files changed, 318 insertions, 0 deletions
diff --git a/comm/mailnews/imap/src/ImapChannel.jsm b/comm/mailnews/imap/src/ImapChannel.jsm
new file mode 100644
index 0000000000..0e5c333d9a
--- /dev/null
+++ b/comm/mailnews/imap/src/ImapChannel.jsm
@@ -0,0 +1,318 @@
+/* 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 = ["ImapChannel"];
+
+const { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm");
+const { MailChannel } = ChromeUtils.importESModule(
+ "resource:///modules/MailChannel.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * A channel to interact with IMAP server.
+ *
+ * @implements {nsIChannel}
+ * @implements {nsIRequest}
+ * @implements {nsICacheEntryOpenCallback}
+ */
+class ImapChannel extends MailChannel {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMailChannel",
+ "nsIChannel",
+ "nsIRequest",
+ "nsIWritablePropertyBag",
+ "nsICacheEntryOpenCallback",
+ ]);
+
+ _logger = ImapUtils.logger;
+ _status = Cr.NS_OK;
+
+ /**
+ * @param {nsIURI} uri - The uri to construct the channel from.
+ * @param {nsILoadInfo} loadInfo - The loadInfo associated with the channel.
+ */
+ constructor(uri, loadInfo) {
+ super();
+ this._server = MailServices.accounts
+ .findServerByURI(uri)
+ .QueryInterface(Ci.nsIImapIncomingServer);
+
+ // nsIChannel attributes.
+ this.originalURI = uri;
+ this.loadInfo = loadInfo;
+ this.contentLength = 0;
+
+ this.uri = uri;
+
+ uri = uri.QueryInterface(Ci.nsIMsgMessageUrl);
+ try {
+ this.contentLength = uri.messageHeader.messageSize;
+ } catch (e) {
+ // Got passed an IMAP folder URL.
+ this._isFolderURL = this._server && !/#(\d+)$/.test(uri.spec);
+ }
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {string}
+ */
+ get name() {
+ return this.URI?.spec;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {boolean}
+ */
+ isPending() {
+ return !!this._pending;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {nsresult}
+ */
+ get status() {
+ return this._status;
+ }
+
+ /**
+ * @see nsICacheEntryOpenCallback
+ */
+ onCacheEntryAvailable(entry, isNew, status) {
+ if (!Components.isSuccessCode(status)) {
+ // If memory cache doesn't work, read from the server.
+ this._readFromServer();
+ return;
+ }
+
+ if (isNew) {
+ if (Services.io.offline) {
+ this._status = Cr.NS_ERROR_OFFLINE;
+ return;
+ }
+ // It's a new entry, needs to read from the server.
+ let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+ let outStream = entry.openOutputStream(0, -1);
+ // When the tee stream receives data from the server, it writes to both
+ // the original listener and outStream (memory cache).
+ tee.init(this._listener, outStream, null);
+ this._listener = tee;
+ this._cacheEntry = entry;
+ this._readFromServer();
+ return;
+ }
+
+ // It's an old entry, read from the memory cache.
+ this._readFromCacheStream(entry.openInputStream(0));
+ }
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ }
+
+ /**
+ * Get readonly URI.
+ * @see nsIChannel
+ */
+ get URI() {
+ return this.uri;
+ }
+
+ get contentType() {
+ return this._contentType || "message/rfc822";
+ }
+
+ set contentType(value) {
+ this._contentType = value;
+ }
+
+ get isDocument() {
+ return true;
+ }
+
+ open() {
+ throw Components.Exception(
+ "ImapChannel.open() not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ asyncOpen(listener) {
+ this._logger.debug(`asyncOpen ${this.URI.spec}`);
+ if (this._isFolderURL) {
+ let handler = Cc[
+ "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder"
+ ].createInstance(Ci.nsIContentHandler);
+ handler.handleContent("x-application-imapfolder", null, this);
+ return;
+ }
+
+ let url = new URL(this.URI.spec);
+ this._listener = listener;
+ if (url.searchParams.get("part")) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ this._listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ listener,
+ this
+ );
+ }
+
+ let msgIds = this.URI.QueryInterface(Ci.nsIImapUrl).QueryInterface(
+ Ci.nsIMsgMailNewsUrl
+ ).listOfMessageIds;
+ this._msgKey = parseInt(msgIds);
+ this.contentLength = 0;
+ try {
+ if (this.readFromLocalCache()) {
+ this._logger.debug("Read from local cache");
+ return;
+ }
+ } catch (e) {
+ this._logger.warn(e);
+ }
+
+ try {
+ let uri = this.URI;
+ if (this.URI.spec.includes("?")) {
+ uri = uri.mutate().setQuery("").finalize();
+ }
+ // Check if a memory cache is available for the current URI.
+ MailServices.imap.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ this.URI.QueryInterface(Ci.nsIImapUrl).storeResultsOffline
+ ? // Don't write to the memory cache if storing offline.
+ Ci.nsICacheStorage.OPEN_READONLY
+ : Ci.nsICacheStorage.OPEN_NORMALLY,
+ this
+ );
+ } catch (e) {
+ this._logger.warn(e);
+ this._readFromServer();
+ }
+ if (this._status == Cr.NS_ERROR_OFFLINE) {
+ throw new Components.Exception(
+ "The requested action could not be completed in the offline state",
+ Cr.NS_ERROR_OFFLINE
+ );
+ }
+ }
+
+ /**
+ * Try to read the message from the offline storage.
+ *
+ * @returns {boolean} True if successfully read from the offline storage.
+ */
+ readFromLocalCache() {
+ if (
+ !this.URI.QueryInterface(Ci.nsIImapUrl).QueryInterface(
+ Ci.nsIMsgMailNewsUrl
+ ).msgIsInLocalCache &&
+ !this.URI.folder.hasMsgOffline(this._msgKey, null, 10)
+ ) {
+ return false;
+ }
+
+ let hdr = this.URI.folder.GetMessageHeader(this._msgKey);
+ let stream = this.URI.folder.getLocalMsgStream(hdr);
+ this._readFromCacheStream(stream);
+ return true;
+ }
+
+ /**
+ * Read the message from the a stream.
+ *
+ * @param {nsIInputStream} cacheStream - The input stream to read.
+ */
+ _readFromCacheStream(stream) {
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this._contentType = "";
+ pump.init(stream, 0, 0, true);
+ pump.asyncRead({
+ onStartRequest: () => {
+ this._listener.onStartRequest(this);
+ this.URI.SetUrlState(true, Cr.NS_OK);
+ this._pending = true;
+ },
+ onStopRequest: (request, status) => {
+ this._listener.onStopRequest(this, status);
+ this.URI.SetUrlState(false, status);
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ this._pending = false;
+ },
+ onDataAvailable: (request, stream, offset, count) => {
+ this.contentLength += count;
+ this._listener.onDataAvailable(this, stream, offset, count);
+ try {
+ if (!stream.available()) {
+ stream.close();
+ }
+ } catch (e) {}
+ },
+ });
+ }
+
+ /**
+ * Retrieve the message from the server.
+ */
+ _readFromServer() {
+ this._logger.debug("Read from server");
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+
+ this._server.wrappedJSObject.withClient(this.URI.folder, client => {
+ client.startRunningUrl(null, null, this.URI);
+ client.channel = this;
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onReady = () => {
+ client.fetchMessage(this.URI.folder, this._msgKey);
+ };
+
+ client.onData = data => {
+ this.contentLength += data.length;
+ outputStream.write(data, data.length);
+ this._listener.onDataAvailable(this, inputStream, 0, data.length);
+ };
+
+ client.onDone = status => {
+ try {
+ this.loadGroup?.removeRequest(this, null, status);
+ } catch (e) {}
+ this._listener.onStopRequest(this, status);
+ };
+ this._pending = false;
+ });
+ }
+
+ /** @see nsIWritablePropertyBag */
+ getProperty(key) {
+ return this[key];
+ }
+
+ setProperty(key, value) {
+ this[key] = value;
+ }
+
+ deleteProperty(key) {
+ delete this[key];
+ }
+}