/* 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/. */
// vim:set ts=2 sw=2 sts=2 et ft=javascript:

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource:///modules/jsmime.jsm");

var EXPORTED_SYMBOLS = ["MimeParser"];

// Emitter helpers, for internal functions later on.
var ExtractHeadersEmitter = {
  startPart: function (partNum, headers) {
    if (partNum == '') {
      this.headers = headers;
    }
  }
};

var ExtractHeadersAndBodyEmitter = {
  body: '',
  startPart: ExtractHeadersEmitter.startPart,
  deliverPartData: function (partNum, data) {
    if (partNum == '')
      this.body += data;
  }
};

var Ci = Components.interfaces;
var Cc = Components.classes;

/// Sets appropriate default options for chrome-privileged environments
function setDefaultParserOptions(opts) {
  if (!("onerror" in opts)) {
    opts.onerror = Components.utils.reportError;
  }
}

var MimeParser = {
  /**
   * Triggers an asynchronous parse of the given input.
   *
   * The input is an input stream; the stream will be read until EOF and then
   * closed upon completion. Both blocking and nonblocking streams are
   * supported by this implementation, but it is still guaranteed that the first
   * callback will not happen before this method returns.
   *
   * @param input   An input stream of text to parse.
   * @param emitter The emitter to receive callbacks on.
   * @param opts    A set of options for the parser.
   */
  parseAsync: function MimeParser_parseAsync(input, emitter, opts) {
    // Normalize the input into an input stream.
    if (!(input instanceof Ci.nsIInputStream)) {
      throw new Error("input is not a recognizable type!");
    }

    // We need a pump for the listener
    var pump = Cc["@mozilla.org/network/input-stream-pump;1"]
                 .createInstance(Ci.nsIInputStreamPump);
    pump.init(input, -1, -1, 0, 0, true);

    // Make a stream listener with the given emitter and use it to read from
    // the pump.
    var parserListener = MimeParser.makeStreamListenerParser(emitter, opts);
    pump.asyncRead(parserListener, null);
  },

  /**
   * Triggers an synchronous parse of the given input.
   *
   * The input is a string that is immediately parsed, calling all functions on
   * the emitter before this function returns.
   *
   * @param input   A string or input stream of text to parse.
   * @param emitter The emitter to receive callbacks on.
   * @param opts    A set of options for the parser.
   */
  parseSync: function MimeParser_parseSync(input, emitter, opts) {
    // We only support string parsing if we are trying to do this parse
    // synchronously.
    if (typeof input != "string") {
      throw new Error("input is not a recognizable type!");
    }
    setDefaultParserOptions(opts);
    var parser = new jsmime.MimeParser(emitter, opts);
    parser.deliverData(input);
    parser.deliverEOF();
    return;
  },

  /**
   * Returns a stream listener that feeds data into a parser.
   *
   * In addition to the functions on the emitter that the parser may use, the
   * generated stream listener will also make calls to onStartRequest and
   * onStopRequest on the emitter (if they exist).
   *
   * @param emitter The emitter to receive callbacks on.
   * @param opts    A set of options for the parser.
   */
  makeStreamListenerParser: function MimeParser_makeSLParser(emitter, opts) {
    var StreamListener = {
      onStartRequest: function SLP_onStartRequest(aRequest, aContext) {
        try {
          if ("onStartRequest" in emitter)
            emitter.onStartRequest(aRequest, aContext);
        } finally {
          this._parser.resetParser();
        }
      },
      onStopRequest: function SLP_onStopRequest(aRequest, aContext, aStatus) {
        this._parser.deliverEOF();
        if ("onStopRequest" in emitter)
          emitter.onStopRequest(aRequest, aContext, aStatus);
      },
      onDataAvailable: function SLP_onData(aRequest, aContext, aStream,
                                           aOffset, aCount) {
        var scriptIn = Cc["@mozilla.org/scriptableinputstream;1"]
                         .createInstance(Ci.nsIScriptableInputStream);
        scriptIn.init(aStream);
        // Use readBytes instead of read to handle embedded NULs properly.
        this._parser.deliverData(scriptIn.readBytes(aCount));
      },
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener,
        Ci.nsIRequestObserver])
    };
    setDefaultParserOptions(opts);
    StreamListener._parser = new jsmime.MimeParser(emitter, opts);
    return StreamListener;
  },

  /**
   * Returns a new raw MIME parser.
   *
   * Prefer one of the other methods where possible, since the input here must
   * be driven manually.
   *
   * @param emitter The emitter to receive callbacks on.
   * @param opts    A set of options for the parser.
   */
  makeParser: function MimeParser_makeParser(emitter, opts) {
    setDefaultParserOptions(opts);
    return new jsmime.MimeParser(emitter, opts);
  },

  /**
   * Returns a dictionary of headers for the given input.
   *
   * The input is any type of input that would be accepted by parseSync. What
   * is returned is a JS object that represents the headers of the entire
   * envelope as would be received by startPart when partNum is the empty
   * string.
   *
   * @param input   A string of text to parse.
   */
  extractHeaders: function MimeParser_extractHeaders(input) {
    var emitter = Object.create(ExtractHeadersEmitter);
    MimeParser.parseSync(input, emitter, {pruneat: '', bodyformat: 'none'});
    return emitter.headers;
  },

  /**
   * Returns the headers and body for the given input message.
   *
   * The return value is an array whose first element is the dictionary of
   * headers (as would be returned by extractHeaders) and whose second element
   * is a binary string of the entire body of the message.
   *
   * @param input   A string of text to parse.
   */
  extractHeadersAndBody: function MimeParser_extractHeaders(input) {
    var emitter = Object.create(ExtractHeadersAndBodyEmitter);
    MimeParser.parseSync(input, emitter, {pruneat: '', bodyformat: 'raw'});
    return [emitter.headers, emitter.body];
  },

  // Parameters for parseHeaderField

  /**
   * Parse the header as if it were unstructured.
   *
   * This results in the same string if no other options are specified. If other
   * options are specified, this causes the string to be modified appropriately.
   */
  HEADER_UNSTRUCTURED:       0x00,
  /**
   * Parse the header as if it were in the form text; attr=val; attr=val.
   *
   * Such headers include Content-Type, Content-Disposition, and most other
   * headers used by MIME as opposed to messages.
   */
  HEADER_PARAMETER:          0x02,
  /**
   * Parse the header as if it were a sequence of mailboxes.
   */
  HEADER_ADDRESS:            0x03,

  /**
   * This decodes parameter values according to RFC 2231.
   *
   * This flag means nothing if HEADER_PARAMETER is not specified.
   */
  HEADER_OPTION_DECODE_2231: 0x10,
  /**
   * This decodes the inline encoded-words that are in RFC 2047.
   */
  HEADER_OPTION_DECODE_2047: 0x20,
  /**
   * This converts the header from a raw string to proper Unicode.
   */
  HEADER_OPTION_ALLOW_RAW:   0x40,

  /// Convenience for all three of the above.
  HEADER_OPTION_ALL_I18N:    0x70,

  /**
   * Parse a header field according to the specification given by flags.
   *
   * Permissible flags begin with one of the HEADER_* flags, which may be or'd
   * with any of the HEADER_OPTION_* flags to modify the result appropriately.
   *
   * If the option HEADER_OPTION_ALLOW_RAW is passed, the charset parameter, if
   * present, is the charset to fallback to if the header is not decodable as
   * UTF-8 text. If HEADER_OPTION_ALLOW_RAW is passed but the charset parameter
   * is not provided, then no fallback decoding will be done. If
   * HEADER_OPTION_ALLOW_RAW is not passed, then no attempt will be made to
   * convert charsets.
   *
   * @param text    The value of a MIME or message header to parse.
   * @param flags   A set of flags that controls interpretation of the header.
   * @param charset A default charset to assume if no information may be found.
   */
  parseHeaderField: function MimeParser_parseHeaderField(text, flags, charset) {
    // If we have a raw string, convert it to Unicode first
    if (flags & MimeParser.HEADER_OPTION_ALLOW_RAW)
      text = jsmime.headerparser.convert8BitHeader(text, charset);

    // The low 4 bits indicate the type of the header we are parsing. All of the
    // higher-order bits are flags.
    switch (flags & 0x0f) {
    case MimeParser.HEADER_UNSTRUCTURED:
      if (flags & MimeParser.HEADER_OPTION_DECODE_2047)
        text = jsmime.headerparser.decodeRFC2047Words(text);
      return text;
    case MimeParser.HEADER_PARAMETER:
      return jsmime.headerparser.parseParameterHeader(text,
        (flags & MimeParser.HEADER_OPTION_DECODE_2047) != 0,
        (flags & MimeParser.HEADER_OPTION_DECODE_2231) != 0);
    case MimeParser.HEADER_ADDRESS:
      return jsmime.headerparser.parseAddressingHeader(text,
        (flags & MimeParser.HEADER_OPTION_DECODE_2047) != 0);
    default:
      throw "Illegal type of header field";
    }
  },
};