Skip to content

Commit

Permalink
Added options argument for parsing to allow to choose if message/rfc8…
Browse files Browse the repository at this point in the history
…22 nodes without disposition header should be treated as attachments or inline
  • Loading branch information
andris9 committed Jul 31, 2024
1 parent 42940ca commit 8ad30b6
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 53 deletions.
94 changes: 48 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

Email parser for browser and serverless environments.

PostalMime can be run in the main web thread or from Web Workers. It can also used in serverless functions.
PostalMime can be run in the main web thread or from Web Workers. It can also be used in serverless functions.

> [!TIP]
> PostalMime is developed by the makers of **[EmailEngine](https://emailengine.app/?utm_source=github&utm_campaign=imapflow&utm_medium=readme-link)** – a self-hosted email gateway that allows making **REST requests against IMAP and SMTP servers**. EmailEngine also sends webhooks whenever something changes on the registered accounts.
## Source

The source code is available from [Github](https://github.com/postalsys/postal-mime).
The source code is available on [GitHub](https://github.com/postalsys/postal-mime).

## Demo

Expand All @@ -23,21 +23,21 @@ First, install the module from npm:
$ npm install postal-mime
```

next import the PostalMime class into your script:
Next, import the PostalMime class into your script:

```js
import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
```

or when using from a Node.js app or in a serverless function:
Or when using it from a Node.js app or in a serverless function:

```js
import PostalMime from 'postal-mime';
```

### Promises

PostalMime methods use Promises, so you need to wait using `await` or wait for the `then()` method to fire until you get the response.
PostalMime methods use Promises, so you need to wait using `await` or the `then()` method to get the response.

#### Browser

Expand Down Expand Up @@ -88,62 +88,64 @@ export default {

#### PostalMime.parse()

`parse(email)` is a static class method to parse emails
`parse(email, options)` is a static class method used to parse emails.

```js
PostalMime.parse(email) -> Promise
PostalMime.parse(email, options) -> Promise
```
Where
Where:
- **email** is the rfc822 formatted email. Either a string, an ArrayBuffer/Uint8Array value, a Blob object, a Node.js Buffer, or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
- **email**: The RFC822 formatted email. This can be a string, an ArrayBuffer/Uint8Array, a Blob object, a Node.js Buffer, or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
- **options**: An optional object containing configuration options.
- **rfc822Attachments**: A boolean (defaults to `false`). If set to `true`, it treats `Message/RFC822` attachments without a Content-Disposition declaration as attachments. By default, these messages are treated as inline values.
This method parses an email message into a structured object with the following properties:
- **headers** is an array of headers in the same order as found from the message (topmost headers first).
- **headers[].key** is lowercase key of the header line, eg. `"dkim-signature"`
- **headers[].value** is the unprocessed value of the header line
- **from**, **sender** includes a processed object for the corresponding headers
- **from.name** is decoded name (empty string if not set)
- **from.address** is the email address
- **deliveredTo**, **returnPath** is the email address from the corresponding header
- **to**, **cc**, **bcc**, **replyTo** includes an array of processed objects for the corresponding headers
- **to[].name** is decoded name (empty string if not set)
- **to[].address** is the email address
- **subject** is the email subject line
- **messageId**, **inReplyTo**, **references** includes the value as found from the corresponding header without any processing
- **date** is the email sending time formatted as an ISO date string (unless parsing failed and in this case the original value is used)
- **html** is the HTML content of the message as a string
- **text** is the plaintext content of the message as a string
- **attachments** is an array that includes message attachments
- **attachment[].filename** is the file name if provided
- **attachment[].mimeType** is the MIME type of the attachment
- **attachment[].disposition** is either "attachment", "inline" or `null` if disposition was not provided
- **attachment[].related** is a boolean value that indicats if this attachment should be treated as embedded image
- **attachment[].contentId** is the ID from Content-ID header
- **attachment[].content** is an Uint8Array value that contains the attachment file
### Utility functions
- **headers**: An array of headers in the order they appear in the message (topmost headers first).
- **headers[].key**: The lowercase key of the header line, e.g., `"dkim-signature"`.
- **headers[].value**: The unprocessed value of the header line.
- **from**, **sender**: Includes a processed object for the corresponding headers.
- **from.name**: The decoded name (empty string if not set).
- **from.address**: The email address.
- **deliveredTo**, **returnPath**: The email address from the corresponding header.
- **to**, **cc**, **bcc**, **replyTo**: An array of processed objects for the corresponding headers.
- **to[].name**: The decoded name (empty string if not set).
- **to[].address**: The email address.
- **subject**: The email subject line.
- **messageId**, **inReplyTo**, **references**: The value as found in the corresponding header without any processing.
- **date**: The email sending time formatted as an ISO date string (unless parsing failed, in which case the original value is used).
- **html**: The HTML content of the message as a string.
- **text**: The plaintext content of the message as a string.
- **attachments**: An array that includes the message attachments.
- **attachments[].filename**: The file name if provided.
- **attachments[].mimeType**: The MIME type of the attachment.
- **attachments[].disposition**: Either "attachment", "inline", or `null` if disposition was not provided.
- **attachments[].related**: A boolean value indicating if this attachment should be treated as an embedded image.
- **attachments[].contentId**: The ID from the Content-ID header.
- **attachments[].content**: A Uint8Array value that contains the attachment file.
### Utility Functions
#### addressParser
Parse email address strings
Parse email address strings.
```js
addressParser(addressStr, opts) -> Array
```

where
Where:

- **addressStr** is the header value for an address header
- **opts** is an optional options object
- **flatten** is a boolean value. If set to `true`, then ignores address groups and returns a flat array of addresses. By default (`flatten` is `false`) the result might include nested groups
- **addressStr**: The header value for an address header.
- **opts**: An optional object containing configuration options.
- **flatten**: A boolean value. If set to `true`, it ignores address groups and returns a flat array of addresses. By default (`flatten` is `false`), the result might include nested groups.

The result is an array of objects
The result is an array of objects:

- **name** is the name string. An empty string is used if name value was not set.
- **address** is the email address value
- **group** is an array of nested address objects. This is used when `flatten` is `false` (the default) and the address string contains address group syntax
- **name**: The name string. An empty string is used if the name value is not set.
- **address**: The email address value.
- **group**: An array of nested address objects. This is used when `flatten` is `false` (the default) and the address string contains address group syntax.

```js
import { addressParser } from 'postal-mime';
Expand All @@ -155,17 +157,17 @@ console.log(addressParser(addressStr));

#### decodeWords

Decode MIME encoded-words
Decode MIME encoded-words.

```js
decodeWords(encodedStr) -> String
```

where
Where:

- **encodedStr** is a string value that _may_ include MIME encoded-words
- **encodedStr**: A string value that _may_ include MIME encoded-words.

The result is a unicode string
The result is a Unicode string.

```js
import { decodeWords } from 'postal-mime';
Expand Down
12 changes: 10 additions & 2 deletions postal-mime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,23 @@ declare type AddressParserOptions = {

declare function addressParser (
str: string,
opts?: AddressParserOptions
options?: AddressParserOptions
): Address[];

declare function decodeWords (
str: string
): string;

declare type PostalMimeOptions = {
rfc822Attachments?: boolean
}

declare class PostalMime {
static parse(email: RawEmail): Promise<Email>;
constructor(options?: PostalMimeOptions);
static parse(
email: RawEmail,
options?: PostalMimeOptions
): Promise<Email>;
parse(email: RawEmail): Promise<Email>;
}

Expand Down
18 changes: 14 additions & 4 deletions src/postal-mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { decodeWords, textEncoder, blobToArrayBuffer } from './decode-strings.js
export { addressParser, decodeWords };

export default class PostalMime {
static parse(buf) {
const parser = new PostalMime();
static parse(buf, options) {
const parser = new PostalMime(options);
return parser.parse(buf);
}

constructor() {
constructor(options) {
this.options = options || {};

this.root = this.currentNode = new MimeNode({
postalMime: this
});
Expand Down Expand Up @@ -129,7 +131,7 @@ export default class PostalMime {
// regular node

// is it inline message/rfc822
if (node.contentType.parsed.value === 'message/rfc822' && node.contentDisposition.parsed.value === 'inline') {
if (this.isInlineMessageRfc822(node)) {
const subParser = new PostalMime();
node.subMessage = await subParser.parse(node.content);

Expand Down Expand Up @@ -339,6 +341,14 @@ export default class PostalMime {
}
}

isInlineMessageRfc822(node) {
if (node.contentType.parsed.value !== 'message/rfc822') {
return false;
}
let disposition = node.contentDisposition.parsed.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
return disposition === 'inline';
}

async resolveStream(stream) {
let chunkLen = 0;
let chunks = [];
Expand Down
11 changes: 10 additions & 1 deletion test/postal-mime-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@ test('Parse calendar email', async t => {
assert.ok(!email.attachments[1].method);
});

test('Parse bounce email', async t => {
test('Parse bounce email inline', async t => {
const mail = await readFile(Path.join(process.cwd(), 'test', 'fixtures', 'bounce.eml'));

const parser = new PostalMime({ rfc822Attachments: true });
const email = await parser.parse(mail);

assert.strictEqual(email.attachments.length, 1);
});

test('Parse bounce email attachment', async t => {
const mail = await readFile(Path.join(process.cwd(), 'test', 'fixtures', 'bounce.eml'));

const parser = new PostalMime();
Expand Down

0 comments on commit 8ad30b6

Please sign in to comment.