Skip to content
LogoLogo

@portable/mime-stream

Functions

MimeStream.splitHeaders

Reads a message's header block from a stream and returns it with a stream of the rest. Nothing is decoded and the body isn't searched for parts, so splitHeaders is the cheap way to get the bytes that DKIM signs. If the input ends before an empty line, all of it is the header block.

async function splitHeaders(
  stream: ReadableStream<Uint8Array>,
  options: splitHeaders.Options = {},
): Promise<splitHeaders.ReturnType>;
OptionTypeDefaultDescription
maxHeaderBytesnumberInfinityLongest header block, counted with the empty line that ends it. Set it, for example to 1 MiB, when the input is untrusted.
signalAbortSignalRejects with signal.reason while the header block is read, and later errors body with it. Either way the input is cancelled.
import * as MimeStream from '@portable/mime-stream';
 
const { headers, body } = await MimeStream.splitHeaders(response.body!);
const bodyHash = await crypto.subtle.digest(
  'SHA-256',
  await new Response(body).arrayBuffer(),
);

Classes

MimeStream.DecoderStream

A TransformStream that splits a raw MIME message into chunks of header blocks, bodies and multipart structure, without decoding anything. Pipe the chunks through MimeStream.EncoderStream to get the same bytes back, or change some chunks on the way.

class DecoderStream extends TransformStream<Uint8Array, Chunk> {
  constructor(options: DecoderStream.Options = {});
}
OptionTypeDefaultDescription
embeddedbooleanfalseRead the message inside a message/rfc822 or message/global part as parts too, when it isn't base64 or quoted-printable.
maxBoundaryLineBytesnumberInfinityLongest boundary line, line break included. Only whitespace after the boundary can make one this long. Set it, for example to 1000, when the input is untrusted.
maxHeaderBytesnumberInfinityLongest header block of a part, counted with the empty line that ends it. Set it, for example to 1 MiB, when the input is untrusted.
maxPartsnumberInfinityMost parts in a message, the root included. Set it, for example to 1000, when the input is untrusted.
signalAbortSignalErrors the stream with signal.reason when aborted, which cancels the source it is piped from.
import * as MimeStream from '@portable/mime-stream';
import * as Streams from '@portable/streams';
 
const chunks = response.body!.pipeThrough(
  new MimeStream.DecoderStream(),
);
for await (const chunk of Streams.toAsyncIterable(chunks)) {
  if (chunk.type === 'headers') {
    console.log(chunk.part.contentType);
  }
}

MimeStream.EncoderStream

A TransformStream that writes chunks from MimeStream.DecoderStream back out as bytes. Chunks that pass through unchanged give back the original message byte for byte.

class EncoderStream extends TransformStream<
  EncoderStream.Input,
  Uint8Array
> {
  constructor(options: EncoderStream.Options = {});
}
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason when aborted, which cancels the source it is piped from.
import * as MimeStream from '@portable/mime-stream';
 
const output = response
  .body!.pipeThrough(new MimeStream.DecoderStream())
  .pipeThrough(new MimeStream.EncoderStream());

Errors

MimeStream.MaxBoundaryLineBytesError

Thrown when a line that matches a boundary grows past maxBoundaryLineBytes, which happens only when that much whitespace follows the boundary.

class MaxBoundaryLineBytesError extends Errors.MaxBytesError {
  constructor(
    options: ErrorOptions & {
      maxBoundaryLineBytes: number;
    },
  );
  readonly kind: string;
  readonly maxBoundaryLineBytes: number;
}
import * as MimeStream from '@portable/mime-stream';
 
const stream = body.pipeThrough(
  new MimeStream.DecoderStream({ maxBoundaryLineBytes: 200 }),
);
// A read rejects with MimeStream.MaxBoundaryLineBytesError on '--boundary' plus 200 spaces.

MimeStream.MaxHeaderBytesError

Thrown when a header block grows past maxHeaderBytes. The count includes the empty line that ends the block. It extends Errors.MaxBytesError, so its maxBytes holds the same limit.

class MaxHeaderBytesError extends Errors.MaxBytesError {
  constructor(
    options: ErrorOptions & {
      maxHeaderBytes: number;
    },
  );
  readonly kind: string;
  readonly maxHeaderBytes: number;
}
import * as MimeStream from '@portable/mime-stream';
 
try {
  await MimeStream.splitHeaders(response.body!, {
    maxHeaderBytes: 64 * 1024,
  });
} catch (error) {
  if (error instanceof MimeStream.MaxHeaderBytesError) {
    console.log(error.maxHeaderBytes);
  }
}

MimeStream.MaxPartsError

Thrown when a message has more than maxParts parts, counting the root.

class MaxPartsError extends Errors.BaseError {
  constructor(
    options: ErrorOptions & {
      maxParts: number;
    },
  );
  readonly kind: string;
  readonly maxParts: number;
}
import * as MimeStream from '@portable/mime-stream';
 
const stream = body.pipeThrough(
  new MimeStream.DecoderStream({ maxParts: 100 }),
);
// A read rejects with MimeStream.MaxPartsError on the 101st part.

Types

MimeStream.Chunk

A run of bytes from the message. Joining every chunk's bytes in order gives back the input.

  • headers: the header block of part, with the empty line that ends it.
  • body: content of a part that has no children, still transfer-encoded.
  • preamble, epilogue: text before the first and after the last delimiter of the multipart part.
  • delimiter: a boundary line of the multipart part, with the line break before it and its own.
type Chunk = {
    type:  |  |  |  | ;
    part: Part;
    bytes: Uint8Array;
};

MimeStream.Part

One part of a message: the root, a part of a multipart, or the message inside a message/rfc822 part.

type Part = {
  parent?: Part | undefined;
  headers: readonly MimeHeaders.Header[];
  contentType: string;
  encoding: string;
  boundary?: string | undefined;
};