/* 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]; } }