A rewrite of multipart library.
authorisaacs <i@izs.me>
Fri, 5 Feb 2010 22:26:44 +0000 (14:26 -0800)
committerRyan Dahl <ry@tinyclouds.org>
Mon, 8 Feb 2010 22:03:06 +0000 (14:03 -0800)
1. Support streaming in the simple case.  Put huge warnings around the
buffering API.
2. Support nested multipart messages.
3. Support various kinds of multipart messages, including all the various
absurdities of multipart email messages.
4. Error out on various invalid types of data that might come through.
5. Gracefully several edge cases that occur when the chunk size is smaller
than the boundary length, or cuts into the \r\n, etc.
6. Provide both positive and negative tests.

doc/api.txt
lib/multipart.js
test/mjsunit/fixtures/multipart.js
test/mjsunit/test-multipart.js

index 66befaa..89995e9 100644 (file)
@@ -1184,73 +1184,158 @@ After emitted no other events will be emitted on the response.
 
 === Multipart Parsing
 
-A library to parse HTTP requests with +multipart/form-data+ is included with
+A library to parse +multipart+ internet messages is included with
 Node.  To use it, +require("multipart")+.
 
-+multipart.parse(options)+ ::
-  - on success: Returns an object where each key holds the value of one part of
-                the stream. +options+ can either be an instance of
-                +http.ServerRequest+ or an object containing a "boundary" and
-                an optional "data" key.
-  - on error: Returns an instanceof Error object. Right now only the request
-              content-type / boundary option is checked. The stream data itself
-              is not validated.
++multipart.parse(message)+ ::
+  Returns a multipart.Stream wrapper around a streaming message.
+  The message must contain a `headers` member, and may be either an
+  HTTP request object or a JSGI-style request object with either a
+  forEachable or String body.
+  +
+  See the Stream class below.
+
++multipart.cat(message)+ ::
+  Returns a promise.
+  - on success: Returns a multipart.Stream object representing the completed
+                message.  The body of each part is saved on the `body` member.
+  - on error: Returns an instanceof Error object.  This indicates
+              that the message was malformed in some way.
+  +
+  *Note*: This function saves the *entire* message into memory.  As such,
+  it is ill-suited to parsing actual incoming messages from an HTTP request!
+  If a user uploads a very large file, then it may cause serious problems.
+  No checking is done to ensure that the file does not overload the memory.
+  Only use multipart.cat with known and trusted input!
 
 ==== +multipart.Stream+
 
-Here is an example for parsing a +multipart/form-data+ request:
-
-----------------------------------------
-var multipart = require("multipart");
-var stream = new multipart.Stream(options);
-var parts = {};
-
-stream.addListener("part", function (part) {
-  var buffer = "";
-
-  part.addListener("body", function(chunk) {
-    buffer = buffer + chunk;
-  });
-
-  part.addListener("complete", function() {
-    parts[part.name] = buffer;
-  });
-});
-
-stream.addListener("complete", function() {
-  // The parts object now contains all parts and data
-});
-----------------------------------------
+The multipart.Stream class is a streaming parser wrapped around a message.
+The Stream also contains the properties described for the +part+ objects below,
+and is a reference to the top-level message.
 
+===== Events
 
 [cols="1,2,10",options="header"]
 |=========================================================
 |Event           | Parameters   | Notes
-|+"part"+        | +part+       | Emitted when a new part is found in the stream.
-                                  +part+ is an instance of +multipart.Part+.
+|+"partBegin"+   | +part+       | Emitted when a new part is found in the stream.
+                                  +part+ is a +part object+, described below.
+|+"partEnd"+     | +part+       | Emitted when a part is done.
+|+"body"+        | +chunk+      | Emitted when a chunk of the body is read.
 |+"complete"+    |              | Emitted when the end of the stream is reached.
+|+"error"+       | +error+      | Emitted when a parse error is encountered. This
+                                  indicates that the message is malformed.
 |=========================================================
 
-+stream.bytesTotal+::
-The amount of bytes this stream is expected to have.
+===== Properties
 
-+stream.bytesReceived+::
-The amount of bytes received by this stream so far.
++stream.part+::
+The current part being processed.  This is important, for instance, when responding
+to the +body+ event.
 
-==== +multipart.Part+
++stream.isMultiPart+::
+True if the stream is a multipart message.  Generally this will be true, but non-multipart
+messages will behave the same as a multipart message with a single part, and +isMultiPart+
+will be set to +false+.
 
-[cols="1,2,10",options="header"]
-|=========================================================
-|Event           | Parameters   | Notes
-|+"body"+        | +chunk+      | Emitted when a chunk of body is read.
-|+"complete"+    |              | Emitted when the end of the part is reached.
-|=========================================================
++stream.parts+::
+An array of the parts contained within the message.  Each is a +part+ object.
 
-+part.name+::
-The field name of this part.
+===== Methods
+
++stream.pause+::
+If the underlying message supports pause and resume, then this will pause the stream.
+
++stream.resume+::
+If the underlying message supports pause and resume, then this will resume the paused stream.
+
+==== Part Objects
+
+As it parses the message, the Stream object will create +Part+ objects.
+
+===== Properties
+
++part.parent+::
+The message that contains this part.
+
++part.headers+::
+The headers object for this message.
 
 +part.filename+::
-The filename of this part. Only set for file uploads.
+The filename, if specified in the +content-disposition+ or +content-type+ header.
+For uploads, downloads, and attachments, this is the intended filename for the
+attached file.
+
++part.name+::
+The name, if specified in the +content-disposition+ or +content-type+ header.  For
++multipart/form-data+ messages, this is the name of the field that was posted, and the
+body specifies the value.
+
++part.isMultiPart+::
+True if this part is a multipart message.
+
++part.parts+::
+Array of children contained within a multipart message, or falsey.
+
++part.boundary+::
+For multipart messages, this is the boundary that separates subparts.
+
++part.type+::
+For multipart messages, this is the multipart type specified in the +content-type+ header.
+For example, a message with +content-type: multipart/form-data+ will have a +type+
+property of +form-data+.
+
+==== Example
+
+Here is an example for parsing a +multipart/form-data+ request:
+
+----------------------------------------
+var multipart = require("multipart"),
+  sys = require("sys"),
+  http = require("http");
+http.createServer(function (req, res) {
+  var mp = multipart.parse(req),
+    fields = {},
+    name, filename;
+  mp.addListener("error", function (er) {
+    res.sendHeader(400, {"content-type":"text/plain"});
+    res.sendBody("You sent a bad message!\n"+er.message);
+    res.finish();
+  });
+  mp.addListener("partBegin", function (part) {
+    name = part.name;
+    filename = part.filename;
+    if (name) fields[name] = "";
+  });
+  mp.addListener("body", function (chunk) {
+    if (name) {
+      // just a demo.  in reality, you'd probably
+      // want to sniff for base64 encoding, decode,
+      // and write the bytes to a file or something.
+      if (fields[name].length > 1024) return;
+      fields[name] += chunk;
+    }
+  });
+  mp.addListener("complete", function () {
+    var response = "You posted: \n" + sys.inspect(fields);
+    res.sendHeader(200, {
+      "content-type" : "text/plain",
+      "content-length" : response.length
+    });
+    res.sendBody(response);
+    res.finish();
+  })
+});
+----------------------------------------
+
+==== Nested Multipart Messages
+
+Nested multipart parsing is supported. The +stream.part+ object always refers
+to the current part. If +part.isMultiPart+ is set, then that part is a
+multipart message, which contains other parts. You can inspect its +parts+
+array to see the list of sub-parts, which may also be multipart, and contain
+sub-parts.
 
 === TCP
 
index dc2a8ce..15fb068 100644 (file)
-var sys = require("sys");
-var events = require('events');
-
-exports.parse = function(options) {
-  var promise = new events.Promise();
-
-  try {
-    var stream = new exports.Stream(options);
-  } catch (e) {
-    process.nextTick(function() {
-      promise.emitError(e);
-    });
-    return promise;
-  }
-
-  var parts = {};
-  stream.addListener('part', function(part) {
-    var name = part.name;
-    var buffer = '';
 
-    part.addListener('body', function(chunk) {
-      buffer = buffer + chunk;
-    });
+var sys = require("sys"),
+  events = require("events"),
+  wrapExpression = /^[ \t]+/,
+  multipartExpression = new RegExp(
+    "^multipart\/(" +
+    "mixed|rfc822|message|digest|alternative|" +
+    "related|report|signed|encrypted|form-data|" +
+    "x-mixed-replace|byteranges)", "i"),
+  boundaryExpression = /boundary=([^;]+)/i,
+  CR = "\r",
+  LF = "\n",
+  CRLF = CR+LF,
+  MAX_BUFFER_LENGTH = 16 * 1024,
+
+  // parser states.
+  s = 0,
+  S_NEW_PART = s++,
+  S_HEADER = s++,
+  S_BODY = s++;
+
+exports.parse = parse;
+exports.cat = cat;
+exports.Stream = Stream;
+
+// Parse a streaming message to a stream.
+// If the message has a "body" and no "addListener", then
+// just take it in and write() the body.
+function parse (message) {
+  return new Stream(message);
+};
 
-    part.addListener('complete', function() {
-      parts[name] = buffer;
-    });
+// WARNING: DONT EVER USE THE CAT FUNCTION IN PRODUCTION WEBSITES!!
+// It works pretty great, and it's a nice test function. But if
+// you use this function to parse an HTTP request from a live web
+// site, then you're essentially giving the world permission to
+// rack up as much memory usage as they can manage.  This function
+// buffers the whole message, which is very convenient, but also
+// very much the wrong thing to do in most cases.
+function cat (message) {
+  var p = new (events.Promise),
+    stream = parse(message);
+  stream.files = {};
+  stream.fields = {};
+  stream.addListener("partBegin", function (part) {
+    if (part.filename) stream.files[part.filename] = part;
+    if (part.name) stream.fields[part.name] = part;
   });
-
-  stream.addListener('complete', function() {
-    promise.emitSuccess(parts);
+  stream.addListener("body", function (chunk) {
+    stream.part.body = (stream.part.body || "") + chunk;
   });
-
-  return promise;
-};
-
-exports.Stream = function(options) {
-  events.EventEmitter.call(this);
-
-  this.init(options);
+  stream.addListener("error", function (e) { p.emitError(e) });
+  stream.addListener("complete", function () { p.emitSuccess(stream) });
+  return p;
 };
-sys.inherits(exports.Stream, events.EventEmitter);
-
-
-var proto = exports.Stream.prototype;
-
-proto.init = function(options) {
-  this.buffer = '';
-  this.bytesReceived = 0;
-  this.bytesTotal = 0;
-  this.part = null;
 
-  if ('headers' in options) {
-    var req = options, contentType = req.headers['content-type'];
-    if (!contentType) {
-      throw new Error('Content-Type header not set');
+// events:
+// "partBegin", "partEnd", "body", "complete"
+// everything emits on the Stream directly.
+// the stream's "parts" object is a nested collection of the header objects
+// check the stream's "part" member to know what it's currently chewin on.
+// this.part.parent refers to that part's containing message (which may be
+// the stream itself)
+// child messages inherit their parent's headers
+// A non-multipart message looks just like a multipart message with a
+// single part.
+function Stream (message) {
+  var isMultiPart = multipartHeaders(message, this),
+    w = isMultiPart ? writer(this) : simpleWriter(this),
+    e = ender(this);
+  if (message.addListener) {
+    message.addListener("body", w);
+    message.addListener("complete", e);
+    if (message.pause && message.resume) {
+      this._pause = message;
     }
-
-    if (!contentType.match(/^multipart\/form-data/i)) {
-      throw new Error('Content-Type is not multipart: "'+contentType+'"');
+  } else if (message.body) {
+    var self = this;
+    if (message.body.pause && message.body.resume) {
+      this._pause = message.body;
     }
-
-    var boundary = contentType.match(/boundary=([^;]+)/i)
-    if (!boundary) {
-      throw new Error('No boundary in Content-Type header: "'+contentType+'"');
+    if (message.body.addListener) {
+      message.body.addListener("data", w);
+      message.body.addListener("end", e);
+    } if (message.body.forEach) {
+      var p = message.body.forEach(w);
+      if (p && p.addCallback) p.addCallback(e);
+      else e();
+    } else {
+      // just write a string.
+      w(message.body);
+      e();
     }
+  }
+};
+Stream.prototype = {
+  __proto__ : events.EventEmitter.prototype,
+  error : function (ex) {
+    this._error = ex;
+    this.emit("error", ex);
+  },
+  pause : function () {
+    if (this._pause) return this._pause.pause();
+    throw new Error("Unsupported");
+  },
+  resume : function () {
+    if (this._pause) return this._pause.resume();
+    throw new Error("Unsupported");
+  }
+};
 
-    this.boundary = '--'+boundary[1];
-    this.bytesTotal = req.headers['content-length'];
-
-    var self = this;
-    req
-      .addListener('body', function(chunk) {
-        self.write(chunk);
-      })
-      .addListener('complete', function() {
-        self.emit('complete');
-      });
-  } else {
-    if (!options.boundary) {
-      throw new Error('No boundary option given');
+// check the headers of the message.  If it wants to be multipart,
+// then we'll be returning true.  Regardless, if supplied, then
+// stream will get a headers object that inherits from message's.
+// If no stream object is supplied, then this function just inspects
+// the message's headers for multipartness, and modifies the message
+// directly.  This divergence is so that we can avoid modifying
+// the original message when we want a wrapper, but still have the
+// info available when it's one of our own objects.
+function multipartHeaders (message, stream) {
+  var field, val, contentType, contentDisposition = "";
+  if (stream) stream.headers = {};
+  for (var h in message.headers) if (message.headers.hasOwnProperty(h)) {
+    val = message.headers[h];
+    field = h.toLowerCase();
+    if (stream) stream.headers[field] = val;
+    if (field === "content-type") {
+      contentType = val;
+    } else if (field === "content-disposition") {
+      contentDisposition = val;
     }
-
-    this.boundary = options.boundary;
-    this.write(options.data || '');
   }
-};
 
-proto.write = function(chunk) {
-  this.bytesReceived = this.bytesReceived + chunk.length;
-  this.buffer = this.buffer + chunk;
-
-  while (this.buffer.length) {
-    var offset = this.buffer.indexOf(this.boundary);
-
-    if (offset === 0) {
-      this.buffer = this.buffer.substr(offset + this.boundary.length + 2);
-    } else if (offset == -1) {
-      if (this.buffer === "\r\n") {
-        this.buffer = '';
-      } else {
-        this.part = (this.part || new Part(this));
-        this.part.write(this.buffer);
-        this.buffer = [];
+  if (!Array.isArray(contentDisposition)) {
+    contentDisposition = contentDisposition.split(",");
+  }
+  contentDisposition = contentDisposition[contentDisposition.length - 1];
+
+  var mutate = (stream || message);
+
+  // Name and filename can come along with either content-disposition
+  // or content-type.  Well-behaved agents use CD rather than CT,
+  // but sadly not all agents are well-behaved.
+  [contentDisposition, contentType].forEach(function (h) {
+    if (!h) return;
+    var cd = h.split(/; */);
+    cd.shift();
+    for (var i = 0, l = cd.length; i < l; i ++) {
+      var bit = cd[i].split("="),
+        name = bit.shift(),
+        val = stripQuotes(bit.join("="));
+      if (name === "filename" || name === "name") {
+        mutate[name] = val;
       }
-    } else if (offset > 0) {
-      this.part = (this.part || new Part(this));
-      this.part.write(this.buffer.substr(0, offset - 2));
+    }
+  });
 
-      this.part.emit('complete');
+  if (!contentType) {
+    return false;
+  }
 
-      this.part = new Part(this);
-      this.buffer = this.buffer.substr(offset + this.boundary.length + 2);
-    }
+  // legacy
+  // TODO: Update this when/if jsgi-style headers are supported.
+  // this will keep working, but is less efficient than it could be.
+  if (!Array.isArray(contentType)) {
+    contentType = contentType.split(",");
   }
-};
+  contentType = contentType[contentType.length-1];
 
-function Part(stream) {
-  events.EventEmitter.call(this);
+  // make sure it's actually multipart.
+  var mpType = multipartExpression.exec(contentType);
+  if (!mpType) {
+    return false;
+  }
 
-  this.headers = {};
-  this.name = null;
-  this.filename = null;
-  this.buffer = '';
-  this.bytesReceived = 0;
+  // make sure we have a boundary.
+  var boundary = boundaryExpression.exec(contentType);
+  if (!boundary) {
+    return false;
+  }
 
-  // Avoids turning Part into a circular JSON object
-  this.getStream = function() {
-    return stream;
-  };
+  mutate.type = mpType[1];
+  mutate.boundary = "--" + boundary[1];
+  mutate.isMultiPart = true;
 
-  this._headersComplete = false;
+  return true;
+};
+function simpleWriter (stream) {
+  stream.part = stream;
+  stream.type = false;
+  var started = false;
+  return function (chunk) {
+    if (!started) {
+      stream.emit("partBegin", stream);
+      started = true;
+    }
+    stream.emit("body", chunk);
+  };
 }
-sys.inherits(Part, events.EventEmitter);
-
-Part.prototype.parsedHeaders = function() {
-  for (var header in this.headers) {
-    var parts = this.headers[header].split(/; ?/), parsedHeader = {};
-    for (var i = 0; i < parts.length; i++) {
-      var pair = parts[i].split('=');
-      if (pair.length < 2) {
+function writer (stream) {
+  var buffer = "",
+    state = S_NEW_PART,
+    part = stream.part = stream;
+  stream.parts = [];
+  stream.parent = stream;
+  return function (chunk) {
+    if (stream._error) return;
+    // write to the buffer, and then process the buffer.
+    buffer += chunk;
+    while (buffer.length > 0) {
+      while (buffer.substr(0, 2) === CRLF) buffer = buffer.substr(2);
+      switch (state) {
+        case S_NEW_PART:
+          // part is a multipart message.
+          // we're either going to start reading a new part, or we're going to
+          // end the current part, depending on whether the boundary has -- at
+          // the end.  either way, we expect --boundary right away.
+          var boundary = part.boundary,
+            len = boundary.length,
+            offset = buffer.indexOf(boundary);
+          if (offset === -1) {
+            if (buffer.length > MAX_BUFFER_LENGTH) {
+              return stream.error(new Error(
+                "Malformed: boundary not found at start of message"));
+            }
+            // keep waiting for it.
+            return;
+          }
+          if (offset > 0) {
+            return stream.error(Error("Malformed: data before the boundary"));
+          }
+          if (buffer.length < (len + 2)) {
+            // we'll need to see either -- or CRLF after the boundary.
+            // get it on the next pass.
+            return;
+          }
+          if (buffer.substr(len, 2) === "--") {
+            // this message is done.
+            // chomp off the boundary and crlf and move up
+            if (part !== stream) {
+              // wait to see the crlf, unless this is the top-level message.
+              if (buffer.length < (len + 4)) {
+                return;
+              }
+              if (buffer.substr(len+2, 2) !== CRLF) {
+                return stream.error(new Error(
+                  "Malformed: CRLF not found after boundary"));
+              }
+            }
+            buffer = buffer.substr(len + 4);
+            stream.emit("partEnd", part);
+            stream.part = part = part.parent;
+            state = S_NEW_PART;
+            continue;
+          }
+          if (part !== stream) {
+            // wait to see the crlf, unless this is the top-level message.
+            if (buffer.length < (len + 2)) {
+              return;
+            }
+            if (buffer.substr(len, 2) !== CRLF) {
+              return stream.error(new Error(
+                "Malformed: CRLF not found after boundary"));
+            }
+          }
+          // walk past the crlf
+          buffer = buffer.substr(len + 2);
+          // mint a new child part, and start parsing headers.
+          stream.part = part = startPart(part);
+          state = S_HEADER;
+        continue;
+        case S_HEADER:
+          // just grab everything to the double crlf.
+          var headerEnd = buffer.indexOf(CRLF+CRLF);
+          if (headerEnd === -1) {
+            if (buffer.length > MAX_BUFFER_LENGTH) {
+              return stream.error(new Error(
+                "Malformed: header unreasonably long."));
+            }
+            return;
+          }
+          var headerString = buffer.substr(0, headerEnd);
+          // chomp off the header and the empty line.
+          buffer = buffer.substr(headerEnd + 4);
+          try {
+            parseHeaderString(part.headers, headerString);
+          } catch (ex) {
+            return stream.error(ex);
+          }
+          multipartHeaders(part);
+
+          // let the world know
+          stream.emit("partBegin", part);
+
+          if (part.isMultiPart) {
+            // it has a boundary and we're ready to grab parts out.
+            state = S_NEW_PART;
+          } else {
+            // it doesn't have a boundary, and is about to
+            // start spitting out body bits.
+            state = S_BODY;
+          }
+        continue;
+        case S_BODY:
+          // look for part.parent.boundary
+          var boundary = part.parent.boundary,
+            offset = buffer.indexOf(boundary);
+          if (offset === -1) {
+            // emit and wait for more data, but be careful, because
+            // we might only have half of the boundary so far.
+            // make sure to leave behind the boundary's length, so that we'll
+            // definitely get it next time if it's on its way.
+            var emittable = buffer.length - boundary.length;
+            if (buffer.substr(-1) === CR) emittable -= 1;
+            if (buffer.substr(-2) === CRLF) emittable -= 2;
+
+            if (emittable > 0) {
+              stream.emit("body", buffer.substr(0, emittable));
+              buffer = buffer.substr(emittable);
+            }
+            // haven't seen the boundary, so wait for more bytes.
+            return;
+          }
+          if (offset > 0) {
+            var emit = buffer.substr(0, offset);
+            if (emit.substr(-2) === CRLF) emit = emit.substr(0, emit.length-2);
+            if (emit) stream.emit("body", emit);
+            buffer = buffer.substr(offset);
+          }
+
+          // let em know we're done.
+          stream.emit("partEnd", part);
+
+          // now buffer starts with boundary.
+          if (buffer.substr(boundary.length, 2) === "--") {
+            // message end.
+            // parent ends, look for a new part in the grandparent.
+            stream.part = part = part.parent;
+            stream.emit("partEnd", part);
+            stream.part = part = part.parent;
+            state = S_NEW_PART;
+            buffer = buffer.substr(boundary.length + 4);
+          } else {
+            // another part coming for the parent message.
+            stream.part = part = part.parent;
+            state = S_NEW_PART;
+          }
         continue;
       }
-
-      var key = pair[0].toLowerCase(), val = pair[1] || '';
-      val = stripslashes(val).substr(1);
-      val = val.substr(0, val.length - 1);
-
-      parsedHeader[key] = val;
     }
+  };
+};
 
-    if (header == 'content-disposition') {
-      this.name = parsedHeader.name || null;
-      this.filename = parsedHeader.filename || null;
+function parseHeaderString (headers, string) {
+  var lines = string.split(CRLF),
+    field, value, line;
+  for (var i = 0, l = lines.length; i < l; i ++) {
+    line = lines[i];
+    if (line.match(wrapExpression)) {
+      if (!field) {
+        throw new Error("Malformed. First header starts with whitespace.");
+      }
+      value += line.replace(wrapExpression, " ");
+      continue;
+    } else if (field) {
+      // now that we know it's not wrapping, put it on the headers obj.
+      affixHeader(headers, field, value);
     }
-
-    this.headers[header] = parsedHeader;
+    line = line.split(":");
+    field = line.shift().toLowerCase();
+    if (!field) {
+      throw new Error("Malformed: improper field name.");
+    }
+    value = line.join(":").replace(/^\s+/, "");
   }
+  // now affix the last field.
+  affixHeader(headers, field, value);
 };
 
-Part.prototype.write = function(chunk) {
-  if (this._headersComplete) {
-    this.bytesReceived = this.bytesReceived + chunk.length;
-    this.emit('body', chunk);
-    return;
+function affixHeader (headers, field, value) {
+  if (!headers.hasOwnProperty(field)) {
+    headers[field] = value;
+  } else if (Array.isArray(headers[field])) {
+    headers[field].push(value);
+  } else {
+    headers[field] = [headers[field], value];
   }
+};
 
-  this.buffer = this.buffer + chunk;
-  while (this.buffer.length) {
-    var offset = this.buffer.indexOf("\r\n");
-
-    if (offset === 0) {
-      this._headersComplete = true;
-      this.parsedHeaders();
-      this.getStream().emit('part', this);
-
-      this.buffer = this.buffer.substr(2);
-      this.bytesReceived = this.bytesReceived + this.buffer.length;
-      this.emit('body', this.buffer);
-      this.buffer = '';
-      return;
-    } else if (offset > 0) {
-      var header = this.buffer.substr(0, offset).split(/: ?/);
-      this.headers[header[0].toLowerCase()] = header[1];
-      this.buffer = this.buffer.substr(offset+2);
-    } else if (offset === -1) {
-      return;
-    }
-  }
+function startPart (parent) {
+  var part = {
+    headers : {},
+    parent : parent
+  };
+  parent.parts = parent.parts || [];
+  parent.parts.push(part);
+  return part;
 };
 
+function ender (stream) { return function () {
+  if (stream._error) return;
+  if (!stream.isMultiPart) stream.emit("partEnd", stream);
+  stream.emit("complete");
+}};
+
 function stripslashes(str) {
   // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
   // +   improved by: Ates Goral (http://magnetiq.com)
@@ -198,20 +412,24 @@ function stripslashes(str) {
   // +   improved by: rezna
   // +   input by: Rick Waldron
   // +   reimplemented by: Brett Zamir (http://brett-zamir.me)
-  // *     example 1: stripslashes('Kevin\'s code');
+  // *     example 1: stripslashes("Kevin\'s code");
   // *     returns 1: "Kevin's code"
-  // *     example 2: stripslashes('Kevin\\\'s code');
+  // *     example 2: stripslashes("Kevin\\\'s code");
   // *     returns 2: "Kevin\'s code"
-  return (str+'').replace(/\\(.?)/g, function (s, n1) {
+  return (str+"").replace(/\\(.?)/g, function (s, n1) {
     switch(n1) {
-      case '\\':
-        return '\\';
-      case '0':
-        return '\0';
-      case '':
-        return '';
+      case "\\":
+        return "\\";
+      case "0":
+        return "\0";
+      case "":
+        return "";
       default:
         return n1;
     }
   });
-}
+};
+function stripQuotes (str) {
+  str = stripslashes(str);
+  return str.substr(1, str.length - 2);
+};
index 51212ba..726fec7 100644 (file)
-exports.reply = ["--AaB03x\r",
-"content-disposition: form-data; name=\"reply\"\r",
-"\r",
-"yes\r",
-"--AaB03x\r",
-"content-disposition: form-data; name=\"fileupload\"; filename=\"dj.jpg\"\r",
-"Content-Type: image/jpeg\r",
-"Content-Transfer-Encoding: base64\r",
-"\r",
-"/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r",
-"--AaB03x--\r\n"].join("\n");
\ No newline at end of file
+
+// each message contains a header, body, and a list of the parts that are
+// expected.  Any properties in the expected objects will be matched against
+// the parsed parts.
+
+var messages = exports.messages = [];
+
+var bad = exports.badMessages = [];
+
+var longString = "";
+for (var i = 0; i < (16*1024); i ++) longString += Math.random();
+
+// content before the first boundary
+bad.push({
+  headers : { "Content-Type":"multipart/mixed; boundary=boundary" },
+  body : "blerg\r\n--boundary\r\nblarggghhh"
+});
+// no boundary
+bad.push({
+  headers : { "Content-Type":"multipart/mixed; boundary=boundary" },
+  body : longString
+});
+// header unreasonably long.
+bad.push({
+  headers : { "Content-Type":"multipart/mixed; boundary=boundary" },
+  body : "--boundary\r\ncontent-type: "+longString+"\r\n"+longString
+});
+// CRLF not found after boundary
+bad.push({
+  headers : { "Content-Type":"multipart/mixed; boundary=boundary" },
+  body : "--boundary"+longString
+});
+// start first header with whitespace.
+bad.push({
+  headers : { "Content-Type":"multipart/mixed; boundary=boundary" },
+  body : "--boundary\r\n  fail: blahrg\r\n\r\n"+longString
+});
+
+// The comments in this first test case tell a story about what the parser is
+// doing at each step.  If you mean to touch the code, it's best to read through
+// this test case first so that you know what you're getting into.
+messages.push({
+  expect : [
+    { type : "mixed", boundary : "--inner1" },
+    { type : "mixed", boundary : "--inner2" },
+    { filename : "hello.txt" },
+    { filename : "hello2.txt" },
+    { type : "mixed", boundary : "--inner3" },
+    { filename : "hello3.txt" },
+    { filename : "hello4.txt" },
+    { filename : "hello-outer.txt" }
+  ],
+  headers : {
+    "Content-Type":"multipart/mixed; boundary=outer"
+  }, body : [
+    // s=new part, part = stream, part.boundary=--outer
+    "--outer",// chomp to here, because it matches the boundary.
+      // mint a new part without a boundary, parent=old part, set state to header
+      "Content-Type: multipart/mixed; boundary=inner1",// move along
+      "", // found the end of the header.  chomp to here, parse the headers onto
+          // the current part.  Once we do that, we know that the current part
+          // is multipart, and has a boundary of --inner1
+          // s=new part, part = --inner1
+      "--inner1", // chomp to here.
+        // mint a new part without a boundary, parent=--inner1, s=header
+        "Content-type: multipart/mixed; boundary=inner2", // move along
+        "", // again, found the end of the header.  chomp to here, parse headers
+            // onto the newly minted part.  Then find out that this part has a
+            // boundary of --inner2.
+            // s=new part, part=--inner2
+        "--inner2", // chomp to here.
+          // mint a new part without a boundary, parent=--inner2
+          "Content-type: text/plain", // move along
+          "content-disposition: inline; filename=\"hello.txt\"", // move along
+          "", // chomp to here.  found end of header.  parse headers
+              // then we know that it's not multipart, so we'll be looking for
+              // the parent's boundary and emitting body bits.
+              // also, we can set part.filename to "hello.txt"
+              // s=body, part=hello.txt
+          "hello, world", // chomp, emit the body, looking for parent-boundary
+        "--inner2", // found parent.boundary.  leave it on the buffer, and
+                    // set part=part.parent, s=new part
+                    // on the next pass, we'll chomp to here, mint a new part
+                    // without a boundary, set s=header
+          "content-type: text/plain", // header...
+          "content-disposition: inline; filename=\"hello2.txt\"", // header...
+          "", // chomp to here, parse header onto the current part.
+              // since it's not multipart, we're looking for parent.boundary
+          "hello to the world", // body, looking for parent.boundary=--inner
+        "--inner2--", // found parent.boundary.  In this case, we have the
+                      // trailing --, indicating that no more parts are coming
+                      // for this set.  We need to back up to the grandparent,
+                      // and then do the new part bit.  Chomp off the --inner2--
+                      // s=new part, part=part.parent.parent=--inner1
+      "--inner1", // chomp to here, because this is part.boundary
+                  // mint a new part without a boundary
+                  // s=header, part = (new)
+        "Content-type: multipart/mixed; boundary=inner3", // header...
+        "", // chomp to here, parse headers onto the new part.
+            // it's multipart, so set the boundary=--inner3,
+            // s=new part, part = --inner3
+        "--inner3", // chomp to here.  mint a new part with no boundary, parse headers
+          "Content-type: text/plain", // header
+          "content-disposition: inline; filename=\"hello3.txt\"", // header
+          "", // end of header. parse headers onto part, whereupon we find that it is
+              // not multipart, and has a filename of hello3.txt.
+              // s=body, part=hello3.txt, looking for part.parent.boundary=--inner3
+          "hello, free the world", // body...
+        "--inner3", // found parent.boundary, and it's not the end.
+                    // s = new part, part = part.parent
+                    // next pass:
+                    // mint a new part without a boundary, s=header
+          "content-type: text/plain", // header
+          "content-disposition: inline; filename=\"hello4.txt\"", // header
+          "", // chomp to here, parse headers on to boundaryless part.
+              // s=body, part = hello4.txt
+          "hello for the world", // body, looking for part.parent.boundary=--inner3
+        "--inner3--", // found parent.boundary, and it's the end
+                      // chomp this off the buffer, part = part.parent.parent=--inner1
+                      // s = new part
+      "--inner1", // chomp to here, because part.boundary = --inner1
+                  // mint a new boundariless part, s = header
+        "Content-type: text/plain", // header...
+        "content-disposition: inline; filename=\"hello-outer.txt\"", // header...
+        "", // chomp to here, parse headers onto the current part.
+            // has no boundary, so we're gonna go into body mode.
+            // s = body, boundary = parent.boundary = --inner1, part = hello-outer.txt
+        "hello, outer world", // body, looking for parent.boundary=--inner1
+      "--inner1--", // found the parent.boundary, and it's the end.
+                    // chomp off the --inner1--, part = part.parent.parent, s = new part
+    "--outer--" // we're looking for a new part, but found the ending.
+                // chomp off the --outer--, part = part.parent, s = new part.
+  ].join("\r\n")
+});
+
+messages.push({
+  headers : {
+    "Content-Type": "multipart/form-data; boundary=AaB03x",
+  },
+  body : [
+    "--AaB03x",
+    "content-disposition: form-data; name=\"reply\"",
+    "",
+    "yes",
+    "--AaB03x",
+    "content-disposition: form-data; name=\"fileupload\"; filename=\"dj.jpg\"",
+    "Content-Type: image/jpeg",
+    "Content-Transfer-Encoding: base64",
+    "",
+    "/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg",
+    "--AaB03x--", ""
+  ].join("\r\n"),
+  expect : [
+    { name : "reply" },
+    { name : "fileupload", filename : "dj.jpg" }
+  ]
+});
+
+// one that's not multipart, just for kicks.
+// verify that it ducks as a multipart message with one part.
+messages.push({
+  headers: { "content-type" : "text/plain" },
+  body : "Hello, world!",
+
+  // not much to say about this one, since it's just
+  // validating that a part was created, not that it has
+  // any particular properties.
+  expect : [{}]
+});
+
+// An actual email message sent from felixge to isaacs.
+// Addresses and signatures obscured, but the unicycle pic is preserved for posterity.
+messages.push({
+  headers: {
+    // TODO: When node's parser supports header-wrapping, these should actually be wrapped,
+    // because that's how they appear in real life.
+    "Delivered-To":"isaacs...@gmail.com",
+    "Received":"by 10.142.240.14 with SMTP id n14cs252101wfh; Wed, 3 Feb 2010 14:24:08 -0800 (PST)",
+    "Received":"by 10.223.4.139 with SMTP id 11mr194455far.61.1265235847416; Wed, 03 Feb 2010 14:24:07 -0800 (PST)",
+    "Return-Path":"<isaacs+caf_=isaacs...=gmail.com@izs.me>",
+    "Received":"from mail-fx0-f219.google.com (mail-fx0-f219.google.com [209.85.220.219]) by mx.google.com with ESMTP id d13si118373fka.17.2010.02.03.14.24.05; Wed, 03 Feb 2010 14:24:06 -0800 (PST)",
+    "Received-SPF":"neutral (google.com: 209.85.220.219 is neither permitted nor denied by best guess record for domain of isaacs+caf_=isaacs...=gmail.com@izs.me) client-ip=209.85.220.219;",
+    "Authentication-Results":"mx.google.com; spf=neutral (google.com: 209.85.220.219 is neither permitted nor denied by best guess record for domain of isaacs+caf_=isaacs...=gmail.com@izs.me) smtp.mail=isaacs+caf_=isaacs...=gmail.com@izs.me; dkim=pass (test mode) header.i=@gmail.com",
+    "Received":"by mail-fx0-f219.google.com with SMTP id 19so626487fxm.25 for <isaacs...@gmail.com>; Wed, 03 Feb 2010 14:24:05 -0800 (PST)",
+    "Received":"by 10.216.91.15 with SMTP id g15mr146196wef.24.1265235845694; Wed, 03 Feb 2010 14:24:05 -0800 (PST)",
+    "X-Forwarded-To":"isaacs...@gmail.com",
+    "X-Forwarded-For":"isaacs@izs.me isaacs...@gmail.com",
+    "Delivered-To":"i@izs.me",
+    "Received":"by 10.216.12.146 with SMTP id 18cs33122wez; Wed, 3 Feb 2010 14:24:00 -0800 (PST)",
+    "Received":"by 10.213.97.28 with SMTP id j28mr2627124ebn.82.1265235838786; Wed, 03 Feb 2010 14:23:58 -0800 (PST)",
+    "Return-Path":"<hai...@gmail.com>",
+    "Received":"from ey-out-2122.google.com (ey-out-2122.google.com [74.125.78.25]) by mx.google.com with ESMTP id 4si11869270ewy.8.2010.02.03.14.23.54; Wed, 03 Feb 2010 14:23:57 -0800 (PST)",
+    "Received-SPF":"pass (google.com: domain of hai...@gmail.com designates 74.125.78.25 as permitted sender) client-ip=74.125.78.25;",
+    "Received":"by ey-out-2122.google.com with SMTP id d26so431288eyd.17 for <i@izs.me>; Wed, 03 Feb 2010 14:23:54 -0800 (PST)",
+    "DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:mime-version:sender:received:from:date  :x-google-sender-auth:message-id:subject:to:content-type; bh=JXfvYIRzerOieADuqMPGlnlFbIGyPuTssL5icEtSLWw=; b=QDzgOCEbYk8cEdBe+HYx/MJrTWmZyx4qENADOcnnn9Xuk1Q6e/c7b3UsvLf/sMoYrG  z96RQhUVOKi9IAzkQhNnOCWDuF1KNxtFnCGhEXMARXBM3qjXe3QmAqXNhJrI0E9bMeme  d5aX5GMrz5mIark462cDsTmrFgaYE6JtwASho=",
+    "DomainKey-Signature":"a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=mime-version:sender:from:date:x-google-sender-auth:message-id :subject:to:content-type; b=VYkN8OeNNJyxAseCAPH8u2aBfGmZaFesmieoWEDymQ1DsWg/aXbaWt4JGQlefIfmMK  hOXd4EN2/iEix10aWDzKpuUV9gU9Wykm93t3pxD7BCz50Kagwp7NVyDJQLK0H5JSNEU/  IVRp90kKNBsb3v76vPsQydi9awLh/jYrFQVMY=",
+    "MIME-Version":"1.0",
+    "Sender":"hai...@gmail.com",
+    "Received":"by 10.216.86.201 with SMTP id w51mr154937wee.8.1265235834101; Wed, 03 Feb 2010 14:23:54 -0800 (PST)",
+    "From":"Felix Geisendoerfer <f...@debuggable.com>",
+    "Date":"Wed, 3 Feb 2010 23:23:34 +0100",
+    "X-Google-Sender-Auth":"0217977a92fcbed0",
+    "Message-ID":"<56dbc1211002031423g750ba93fs4a2f22ce22431590@mail.gmail.com>",
+    "Subject":"Me on my unicycle",
+    "To":"i@izs.me",
+    "Content-Type":"multipart/mixed; boundary=0016e6d99d0572dfaf047eb9ac2e",
+  },
+  expect : [
+    { type : "alternative", boundary : "--0016e6d99d0572dfa5047eb9ac2c" },
+    {}, // the first bit, text/plain
+    {}, // the second bit, text/html
+    { name : "unicycle.jpg", filename : "unicycle.jpg" }
+  ],
+  body : [
+    "--0016e6d99d0572dfaf047eb9ac2e", // beginpart->header
+    "Content-Type: multipart/alternative; boundary=0016e6d99d0572dfa5047eb9ac2c", // headers. isMultipart
+    "", // bodybegin->beginpart
+    "--0016e6d99d0572dfa5047eb9ac2c",//header
+    "Content-Type: text/plain; charset=ISO-8859-1",
+    "",//bodybegin->body
+    "*This was 4 years ago, I miss riding my unicycle !*",
+    "",
+    "-- fg",
+    "",
+    "--0016e6d99d0572dfa5047eb9ac2c", //partend->partbegin
+    "Content-Type: text/html; charset=ISO-8859-1",
+    "",
+    "<b><font class=\"Apple-style-span\" color=\"#CC0000\">This was 4 years ago, I miss riding my <span class=\"Apple-style-span\" style=\"background-color: rgb(0, 0, 0);\">unicycle</span> !</font></b><div><br clear=\"all\">-- fg<br><br>",
+    "",
+    "",
+    "</div>",
+    "",
+    "--0016e6d99d0572dfa5047eb9ac2c--",//partend, walk up tree-->partbegin
+    "--0016e6d99d0572dfaf047eb9ac2e",//beginpart->header
+    "Content-Type: image/jpeg; name=\"unicycle.jpg\"",//header
+    "Content-Disposition: attachment; filename=\"unicycle.jpg\"",//header
+    "Content-Transfer-Encoding: base64",//header
+    "X-Attachment-Id: f_g58opqah0",//header
+    "",//bodybegin->body
+    "/9j/4AAQSkZJRgABAQEASABIAAD/4gUoSUNDX1BST0ZJTEUAAQEAAAUYYXBwbAIgAABzY25yUkdC",//bodybodybody
+    "IFhZWiAH0wAHAAEAAAAAAABhY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAA",//bodybodybody
+    "AADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAty",//bodybodybody
+    "WFlaAAABCAAAABRnWFlaAAABHAAAABRiWFlaAAABMAAAABR3dHB0AAABRAAAABRjaGFkAAABWAAA",//bodybodybody
+    "ACxyVFJDAAABhAAAAA5nVFJDAAABhAAAAA5iVFJDAAABhAAAAA5kZXNjAAABlAAAAD1jcHJ0AAAE",
+    "1AAAAEFkc2NtAAAB1AAAAv5YWVogAAAAAAAAdEsAAD4dAAADy1hZWiAAAAAAAABacwAArKYAABcm",
+    "WFlaIAAAAAAAACgYAAAVVwAAuDNYWVogAAAAAAAA81IAAQAAAAEWz3NmMzIAAAAAAAEMQgAABd7/",
+    "//MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsY3VydgAAAAAAAAABAjMAAGRlc2MAAAAAAAAAE0Nh",
+    "bWVyYSBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAE0NhbWVyYSBSR0IgUHJvZmlsZQAAAABtbHVjAAAA",
+    "AAAAAA8AAAAMZW5VUwAAACQAAAKeZXNFUwAAACwAAAFMZGFESwAAADQAAAHaZGVERQAAACwAAAGY",
+    "ZmlGSQAAACgAAADEZnJGVQAAADwAAALCaXRJVAAAACwAAAJybmxOTAAAACQAAAIObm9OTwAAACAA",
+    "AAF4cHRCUgAAACgAAAJKc3ZTRQAAACoAAADsamFKUAAAABwAAAEWa29LUgAAABgAAAIyemhUVwAA",
+    "ABoAAAEyemhDTgAAABYAAAHEAEsAYQBtAGUAcgBhAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBpAGwA",
+    "aQBSAEcAQgAtAHAAcgBvAGYAaQBsACAAZgD2AHIAIABLAGEAbQBlAHIAYTCrMOEw6QAgAFIARwBC",
+    "ACAw1zDtMNUwoTCkMOtleE9NdvhqXwAgAFIARwBCACCCcl9pY8+P8ABQAGUAcgBmAGkAbAAgAFIA",
+    "RwBCACAAcABhAHIAYQAgAEMA4QBtAGEAcgBhAFIARwBCAC0AawBhAG0AZQByAGEAcAByAG8AZgBp",
+    "AGwAUgBHAEIALQBQAHIAbwBmAGkAbAAgAGYA/AByACAASwBhAG0AZQByAGEAc3b4ZzoAIABSAEcA",
+    "QgAgY8+P8GWHTvYAUgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABzAGUAIAB0AGkAbAAgAEsAYQBt",
+    "AGUAcgBhAFIARwBCAC0AcAByAG8AZgBpAGUAbAAgAEMAYQBtAGUAcgBhznS6VLd8ACAAUgBHAEIA",
+    "INUEuFzTDMd8AFAAZQByAGYAaQBsACAAUgBHAEIAIABkAGUAIABDAOIAbQBlAHIAYQBQAHIAbwBm",
+    "AGkAbABvACAAUgBHAEIAIABGAG8AdABvAGMAYQBtAGUAcgBhAEMAYQBtAGUAcgBhACAAUgBHAEIA",
+    "IABQAHIAbwBmAGkAbABlAFAAcgBvAGYAaQBsACAAUgBWAEIAIABkAGUAIABsIBkAYQBwAHAAYQBy",
+    "AGUAaQBsAC0AcABoAG8AdABvAAB0ZXh0AAAAAENvcHlyaWdodCAyMDAzIEFwcGxlIENvbXB1dGVy",
+    "IEluYy4sIGFsbCByaWdodHMgcmVzZXJ2ZWQuAAAAAP/hAIxFeGlmAABNTQAqAAAACAAGAQYAAwAA",
+    "AAEAAgAAARIAAwAAAAEAAQAAARoABQAAAAEAAABWARsABQAAAAEAAABeASgAAwAAAAEAAgAAh2kA",
+    "BAAAAAEAAABmAAAAAAAAAEgAAAABAAAASAAAAAEAAqACAAQAAAABAAAAyKADAAQAAAABAAABWwAA",
+    "AAD/2wBDAAICAgICAQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0O",
+    "Dg4OCQsQEQ8OEQ0ODg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O",
+    "Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg7/wAARCAFbAMgDASIAAhEBAxEB/8QAHwAAAQUBAQEB",
+    "AQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1Fh",
+    "ByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZ",
+    "WmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG",
+    "x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAEC",
+    "AwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHB",
+    "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",
+    "CivqJ00meTzNH//Z",
+    "--0016e6d99d0572dfaf047eb9ac2e--",//partend, then complete
+    ""
+  ].join("\r\n")
+});
index 0af864a..fb1ac4a 100644 (file)
 process.mixin(require("./common"));
-http = require("http");
 
-var
+var http = require("http"),
+  multipart = require("multipart"),
+  sys = require("sys"),
   PORT = 8222,
-
-  multipart = require('multipart'),
-  fixture = require('./fixtures/multipart'),
-
-  requests = 0,
-  badRequests = 0,
-  partsReceived = 0,
-  partsComplete = 0,
-
-  respond = function(res, text) {
-    requests++;
-    if (requests == 5) {
-      server.close();
+  fixture = require("./fixtures/multipart"),
+  events = require("events"),
+  testPart = function (expect, part) {
+    if (!expect) {
+      throw new Error("Got more parts than expected: "+
+        JSON.stringify(part.headers));
+    }
+    for (var i in expect) {
+      assert.equal(expect[i], part[i]);
     }
-
-    res.sendHeader(200, {"Content-Type": "text/plain"});
-    res.sendBody(text);
-    res.finish();
   };
 
-var server = http.createServer(function(req, res) {
-  if (req.headers['x-use-simple-api']) {
-    multipart.parse(req)
-      .addCallback(function() {
-        respond(res, 'thanks');
-      })
-      .addErrback(function() {
-        badRequests++;
-        respond(res, 'no thanks');
-      });
-    return;
-  }
-
-
-  try {
-    var stream = new multipart.Stream(req);
-  } catch (e) {
-    badRequests++;
-    respond(res, 'no thanks');
+var emails = fixture.messages.slice(0),
+  chunkSize = 1, // set to minimum to forcibly expose boundary conditions.
+                 // in a real scenario, this would be much much bigger.
+  firstPart = new (events.Promise);
+
+// test streaming messages through directly, as if they were in a file or something.
+(function testEmails () {
+  var email = emails.pop(),
+    curr = 0;
+  if (!email) {
+    firstPart.emitSuccess();
     return;
   }
+  var expect = email.expect;
 
-  var parts = {};
-  stream.addListener('part', function(part) {
-    partsReceived++;
+  var message  = new (events.EventEmitter);
+  message.headers = email.headers;
 
-    var name = part.name;
-
-    if (partsReceived == 1) {
-      assert.equal('reply', name);
-    } else if (partsReceived == 2) {
-      assert.equal('fileupload', name);
+  var mp = multipart.parse(message);
+  mp.addListener("partBegin", function (part) {
+    testPart(email.expect[curr ++], part);
+  });
+  mp.addListener("complete", function () {
+    process.nextTick(testEmails);
+  });
+  // stream it through in chunks.
+  var emailBody = email.body;
+  process.nextTick(function s () {
+    if (emailBody) {
+      message.emit("body", emailBody.substr(0, chunkSize));
+      emailBody = emailBody.substr(chunkSize);
+      process.nextTick(s);
+    } else {
+      message.emit("complete");
     }
-
-    parts[name] = '';
-    part.addListener('body', function(chunk) {
-      parts[name] += chunk;
+  });
+})();
+
+// run good HTTP messages test after previous test ends.
+var secondPart = new (events.Promise),
+  server = http.createServer(function (req, res) {
+    var mp = multipart.parse(req),
+      curr = 0;
+    req.setBodyEncoding("binary");
+    if (req.url !== "/bad") {
+      mp.addListener("partBegin", function (part) {
+        testPart(message.expect[curr ++], part);
+      });
+    }
+    mp.addListener("error", function (er) {
+      res.sendHeader(400, {});
+      res.sendBody("bad");
+      res.finish();
     });
-    part.addListener('complete', function(chunk) {
-      assert.equal(0, part.buffer.length);
-      if (partsReceived == 1) {
-        assert.equal('yes', parts[name]);
-      } else if (partsReceived == 2) {
-        assert.equal(
-          '/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg',
-          parts[name]
-        );
-      }
-      partsComplete++;
+    mp.addListener("complete", function () {
+      res.sendHeader(200, {});
+      res.sendBody("ok");
+      res.finish();
     });
-  });
-
-  stream.addListener('complete', function() {
-    respond(res, 'thanks');
-  });
-});
+  }),
+  message,
+  client = http.createClient(PORT);
 server.listen(PORT);
 
-var client = http.createClient(PORT);
-
-var request = client.request('POST', '/', {
-  'Content-Type': 'multipart/form-data; boundary=AaB03x',
-  'Content-Length': fixture.reply.length
-});
-request.sendBody(fixture.reply, 'binary');
-request.finish();
-
-var simpleRequest = client.request('POST', '/', {
-  'X-Use-Simple-Api': 'yes',
-  'Content-Type': 'multipart/form-data; boundary=AaB03x',
-  'Content-Length': fixture.reply.length
-});
-simpleRequest.sendBody(fixture.reply, 'binary');
-simpleRequest.finish();
-
-var badRequest = client.request('POST', '/', {
-  'Content-Type': 'invalid!',
-  'Content-Length': fixture.reply.length
-});
-badRequest.sendBody(fixture.reply, 'binary');
-badRequest.finish();
-
-var simpleBadRequest = client.request('POST', '/', {
-  'X-Use-Simple-Api': 'yes',
-  'Content-Type': 'something',
-  'Content-Length': fixture.reply.length
-});
-simpleBadRequest.sendBody(fixture.reply, 'binary');
-simpleBadRequest.finish();
-
-var requestWithCharset = client.request('POST', '/', {
-  'X-Use-Simple-Api': 'yes',
-  'Content-Type': 'multipart/form-data; charset=utf-8; boundary=AaB03x',
-  'Content-Length': fixture.reply.length
+// could dry these two up a bit.
+firstPart.addCallback(function testGoodMessages () {
+  var httpMessages = fixture.messages.slice(0);
+  process.nextTick(function testHTTP () {
+    message = httpMessages.pop();
+    if (!message) {
+      secondPart.emitSuccess();
+      return;
+    }
+    var req = client.request("POST", "/", message.headers);
+    req.sendBody(message.body, "binary");
+    req.finish(function (res) {
+      var buff = "";
+      res.addListener("body", function (chunk) { buff += chunk });
+      res.addListener("complete", function () {
+        assert.equal(buff, "ok");
+        process.nextTick(testHTTP);
+      });
+    });
+  });
 });
-requestWithCharset.sendBody(fixture.reply, 'binary');
-requestWithCharset.finish();
-
-process.addListener('exit', function() {
-  puts("done");
-  assert.equal(2, partsComplete);
-  assert.equal(2, partsReceived);
-  assert.equal(2, badRequests);
+secondPart.addCallback(function testBadMessages () {
+  var httpMessages = fixture.badMessages.slice(0);
+  process.nextTick(function testHTTP () {
+    message = httpMessages.pop();
+    if (!message) {
+      server.close()
+      return;
+    }
+    var req = client.request("POST", "/bad", message.headers);
+    req.sendBody(message.body, "binary");
+    req.finish(function (res) {
+      var buff = "";
+      res.addListener("body", function (chunk) { buff += chunk });
+      res.addListener("complete", function () {
+        assert.equal(buff, "bad");
+        process.nextTick(testHTTP);
+      });
+    });
+  });
 });
\ No newline at end of file