FRE-709: Document duplicate recovery wake - FRE-635 already recovered via FRE-708

This commit is contained in:
2026-04-26 20:23:14 -04:00
parent e07237b6b0
commit 0ff6c74871
5880 changed files with 1643723 additions and 908 deletions

276
node_modules/postal-mime/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,276 @@
# Changelog
## [2.7.4](https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4) (2026-03-17)
### Bug Fixes
* add missing originalKey to Header type and Uint8Array to Attachment content ([92cc91c](https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0))
* include originalKey in parsed headers output ([83521c8](https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347))
* preserve __esModule and .default in CJS build for bundler interop ([1466910](https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048))
* prevent RFC 2047 encoded-word address fabrication ([844f920](https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3))
## [2.7.3](https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3) (2026-01-09)
### Bug Fixes
* correct TypeScript type definitions to match implementation ([b225d7c](https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2))
## [2.7.2](https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2) (2026-01-08)
### Bug Fixes
* add null checks for contentType.parsed access ([ad8f4c6](https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395))
* improve RFC compliance for MIME parsing ([e004c3a](https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d))
## [2.7.1](https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1) (2025-12-22)
### Bug Fixes
* Add null checks for contentDisposition.parsed access ([fd54c37](https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add))
## [2.7.0](https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0) (2025-12-22)
### Features
* add headerLines property exposing raw header lines ([c79a02a](https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb))
## [2.6.1](https://github.com/postalsys/postal-mime/compare/v2.6.0...v2.6.1) (2025-11-26)
### Bug Fixes
* prevent DoS from deeply nested address groups ([f509eaf](https://github.com/postalsys/postal-mime/commit/f509eafec31bf448482133041c086f7aefa2b3fa))
* update TypeScript typings to match source code ([df5640a](https://github.com/postalsys/postal-mime/commit/df5640a3166b0a1fe3ca563e1364301bb4e6a025))
## [2.6.0](https://github.com/postalsys/postal-mime/compare/v2.5.0...v2.6.0) (2025-10-24)
### Features
* add CommonJS build support for dual package compatibility ([0ea3de3](https://github.com/postalsys/postal-mime/commit/0ea3de39c713aac2bc79218dfb496c0e2ff4b0b8))
## [2.5.0](https://github.com/postalsys/postal-mime/compare/v2.4.7...v2.5.0) (2025-10-07)
### Features
* add comprehensive type validation and TypeScript support ([81a6467](https://github.com/postalsys/postal-mime/commit/81a6467b6c5379c502bac9ee7023e2c2dee976cb))
## [2.4.7](https://github.com/postalsys/postal-mime/compare/v2.4.6...v2.4.7) (2025-10-07)
### Bug Fixes
* prevent email extraction from quoted strings in addressParser ([837d679](https://github.com/postalsys/postal-mime/commit/837d679b48cde95a111b8508a7aea23a28cbc12a))
## [2.4.6](https://github.com/postalsys/postal-mime/compare/v2.4.5...v2.4.6) (2025-10-01)
### Bug Fixes
* add security limits for MIME parsing ([defbf11](https://github.com/postalsys/postal-mime/commit/defbf11e85c8233e6ff01a3d6fc10534b784c499))
## [2.4.5](https://github.com/postalsys/postal-mime/compare/v2.4.4...v2.4.5) (2025-09-29)
### Bug Fixes
* handle broken quoted-printable sequences and add prettier formatter ([ec3bc64](https://github.com/postalsys/postal-mime/commit/ec3bc647f6e262a7ac3a51e30bbe3b07b6e7db7a))
## [2.4.4](https://github.com/postalsys/postal-mime/compare/v2.4.3...v2.4.4) (2025-06-26)
### Bug Fixes
* **TextDecoder:** Fall back to windows-1252 for an unknown charset instead of throwing ([d5b917d](https://github.com/postalsys/postal-mime/commit/d5b917d5b09fab9183733cd76ebdc896467ae31e))
## [2.4.3](https://github.com/postalsys/postal-mime/compare/v2.4.2...v2.4.3) (2025-01-24)
### Bug Fixes
* **TextDecoder:** Do not reuse text decoders to avoid spilling data from one instance to another ([8b1013e](https://github.com/postalsys/postal-mime/commit/8b1013e52c878020b3705a2e702a560114f4c081))
## [2.4.2](https://github.com/postalsys/postal-mime/compare/v2.4.1...v2.4.2) (2025-01-24)
### Bug Fixes
* **decodeWords:** Better handling of decoded words ([e0f0047](https://github.com/postalsys/postal-mime/commit/e0f0047b6f97e1251f86f7852eb7935882ead0c1))
## [2.4.1](https://github.com/postalsys/postal-mime/compare/v2.4.0...v2.4.1) (2025-01-05)
### Bug Fixes
* **ts:** Tiny schema fix ([f8761e6](https://github.com/postalsys/postal-mime/commit/f8761e6b57af619264fe1ffe121407859e380fe6))
## [2.4.0](https://github.com/postalsys/postal-mime/compare/v2.3.2...v2.4.0) (2025-01-05)
### Features
* **attachments:** Added new option 'attachmentEncoding' to return attachment content as a string, not arraybuffer ([0f7e9df](https://github.com/postalsys/postal-mime/commit/0f7e9df855c9c8f99ed0b7c517d6653169c53405))
## [2.3.2](https://github.com/postalsys/postal-mime/compare/v2.3.1...v2.3.2) (2024-09-23)
### Bug Fixes
* Modified README to trigger a new release (previous npm publish failed) ([f975ef4](https://github.com/postalsys/postal-mime/commit/f975ef4bc8403af72d8cd25ee2d4b3a12cdf82e4))
## [2.3.1](https://github.com/postalsys/postal-mime/compare/v2.3.0...v2.3.1) (2024-09-23)
### Bug Fixes
* **message/rfc822:** Added option 'forceRfc822Attachments' to handle all message/rfc822 nodes as attachments instead of inline content ([bf47621](https://github.com/postalsys/postal-mime/commit/bf47621da7c55a31acb39fb505f415b1ed4ce5e2))
## [2.3.0](https://github.com/postalsys/postal-mime/compare/v2.2.9...v2.3.0) (2024-09-23)
### Features
* Treat message/rfc822 as an attachment for delivery-status and feedback-report ([21e6224](https://github.com/postalsys/postal-mime/commit/21e62245aeb416b921ba683dd8628f5948831c56))
## [2.2.9](https://github.com/postalsys/postal-mime/compare/v2.2.8...v2.2.9) (2024-09-16)
### Bug Fixes
* **exports:** Define 'default' exports as last for legacy compatibility ([a9518c8](https://github.com/postalsys/postal-mime/commit/a9518c8d4cdad2985bf44073534d936612cfc1ae))
## [2.2.8](https://github.com/postalsys/postal-mime/compare/v2.2.7...v2.2.8) (2024-09-14)
### Bug Fixes
* **module:** add default to module exports ([#56](https://github.com/postalsys/postal-mime/issues/56)) ([4f99ebe](https://github.com/postalsys/postal-mime/commit/4f99ebeb48f81848431fcfadafaa4162942c7be8))
## [2.2.7](https://github.com/postalsys/postal-mime/compare/v2.2.6...v2.2.7) (2024-07-31)
### Bug Fixes
* **rfc822:** Only inline message/rfc822 messages if Content-Disposition is ([53024de](https://github.com/postalsys/postal-mime/commit/53024dec22ea121817913a9cf152bdf60acbdbe7))
## [2.2.6](https://github.com/postalsys/postal-mime/compare/v2.2.5...v2.2.6) (2024-07-09)
### Bug Fixes
* **types:** Updated type for attachment.content to ArrayBuffer ([191524f](https://github.com/postalsys/postal-mime/commit/191524fa32ac550934fb17c074153cf9170622a0))
## [2.2.5](https://github.com/postalsys/postal-mime/compare/v2.2.4...v2.2.5) (2024-04-11)
### Bug Fixes
* **types:** Fixed Address type ([57908e4](https://github.com/postalsys/postal-mime/commit/57908e428929904ee312d9e95343a9fbf52542b4))
## [2.2.4](https://github.com/postalsys/postal-mime/compare/v2.2.3...v2.2.4) (2024-04-11)
### Bug Fixes
* **exports:** Export addressParser and decodeWords functions ([43d3187](https://github.com/postalsys/postal-mime/commit/43d31873308d8eff61876f32614e5cc5143c90dd))
## [2.2.3](https://github.com/postalsys/postal-mime/compare/v2.2.2...v2.2.3) (2024-04-11)
### Bug Fixes
* **attachments:** Added description key from Content-Description attachment header ([6e29de9](https://github.com/postalsys/postal-mime/commit/6e29de97a4dc0043587a59870d52250602801e3c))
* **calendar-attachments:** treat text/calendar as an attachment ([2196b49](https://github.com/postalsys/postal-mime/commit/2196b497f289697e9dc72011708e4355ee7362cc))
## [2.2.2](https://github.com/postalsys/postal-mime/compare/v2.2.1...v2.2.2) (2024-04-10)
### Bug Fixes
* **parse:** Do not throw on empty input when initializing an array buffer object ([ddae5b4](https://github.com/postalsys/postal-mime/commit/ddae5b40d44eaebbfc9117609259a204f27ed4cf))
## [2.2.1](https://github.com/postalsys/postal-mime/compare/v2.2.0...v2.2.1) (2024-03-31)
### Bug Fixes
* **parser:** Reply-To value must be an array, not a single address object ([280bd8d](https://github.com/postalsys/postal-mime/commit/280bd8dc1626315e1a43f35641415453c434716e))
## [2.2.0](https://github.com/postalsys/postal-mime/compare/v2.1.2...v2.2.0) (2024-03-26)
### Features
* **interface:** Added statis parse() method to simplify usage (`await PostalMime.parse(email)`) ([c2faa27](https://github.com/postalsys/postal-mime/commit/c2faa276520d6551df640abe008986eebc6d99d3))
## [2.1.2](https://github.com/postalsys/postal-mime/compare/v2.1.1...v2.1.2) (2024-02-29)
### Bug Fixes
* **git:** re-renamed git repo ([29d235e](https://github.com/postalsys/postal-mime/commit/29d235ece222844dc59858d9e991cc85f65733e2))
* **git:** Renamed git repository ([829e537](https://github.com/postalsys/postal-mime/commit/829e5371602f87fe114d87130c6e9953d50872b4))
## [2.1.1](https://github.com/postalsys/postal-mime/compare/v2.1.0...v2.1.1) (2024-02-26)
### Bug Fixes
* **types:** Updated types for PostalMime ([bc90f6d](https://github.com/postalsys/postal-mime/commit/bc90f6d5b7d3e2475cece77bb094caf421dead97))
## [2.1.0](https://github.com/postalsys/postal-mime/compare/v2.0.2...v2.1.0) (2024-02-22)
### Features
* **workers:** Support Cloudflare Email Workers out of the box ([4904708](https://github.com/postalsys/postal-mime/commit/49047089bf779931dacb4a7b31816b48d1b00840))
### Bug Fixes
* **module:** add types to module exports ([#23](https://github.com/postalsys/postal-mime/issues/23)) ([1ee4a42](https://github.com/postalsys/postal-mime/commit/1ee4a427643d71f6a4bda0db0ebe0b5b280e52ae))
## [2.0.2](https://github.com/postalsys/postal-mime/compare/v2.0.1...v2.0.2) (2023-12-08)
### Bug Fixes
* **test:** Added a tests runner and some tests ([8c6f7fb](https://github.com/postalsys/postal-mime/commit/8c6f7fb495b0158756fc11482a717e8081cede86))
* **test:** Added test action ([c43c086](https://github.com/postalsys/postal-mime/commit/c43c0865dae74a7f20e32885a5860d8654f0c932))
## [2.0.1](https://github.com/postalsys/postal-mime/compare/v2.0.0...v2.0.1) (2023-11-05)
### Bug Fixes
* **npm:** DO not ignore src folder when publishing to npm ([ef8a2df](https://github.com/postalsys/postal-mime/commit/ef8a2df8d65be3dcfc52784c5c73c79f820c1c82))
## [2.0.0](https://github.com/postalsys/postal-mime/compare/v1.1.0...v2.0.0) (2023-11-03)
### ⚠ BREAKING CHANGES
* **module:** Use as an ES module, do not build with webpack
### Features
* **module:** Use as an ES module, do not build with webpack ([70df152](https://github.com/postalsys/postal-mime/commit/70df152ed66346d1f0ca821a9caeb819255bea89))
## [1.1.0](https://github.com/postalsys/postal-mime/compare/v1.0.16...v1.1.0) (2023-11-02)
### Features
* **deploy:** automatic publishing ([64f9a81](https://github.com/postalsys/postal-mime/commit/64f9a814414ff4a6f3e33c23a5c4821ab0099c5f))
* **license:** changed license from AGPL to MIT-0 ([d0ca0dc](https://github.com/postalsys/postal-mime/commit/d0ca0dce40315ae63d8ebd6420c0d1467baac01e))
### Bug Fixes
* **ts:** Update postal-mime.d.ts ([#13](https://github.com/postalsys/postal-mime/issues/13)) ([6cee404](https://github.com/postalsys/postal-mime/commit/6cee40477c711959f94def4c33baf4330a6a249f))

16
node_modules/postal-mime/LICENSE.txt generated vendored Normal file
View File

@@ -0,0 +1,16 @@
Copyright (c) 2021-2025 Andris Reinman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

427
node_modules/postal-mime/README.md generated vendored Normal file
View File

@@ -0,0 +1,427 @@
# postal-mime
**postal-mime** is an email parsing library for Node.js, browsers (including Web Workers), and serverless environments (like Cloudflare Email Workers). It takes in a raw email message (RFC822 format) and outputs a structured object containing headers, recipients, attachments, and more.
> [!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 provides a REST API for IMAP and SMTP servers and sends webhooks whenever something changes in registered accounts.
## Features
- **Browser & Node.js compatible** - Works in browsers, Web Workers, Node.js, and serverless environments
- **TypeScript support** - Fully typed with comprehensive type definitions
- **Zero dependencies** - No external dependencies
- **RFC compliant** - Follows RFC 2822/5322 email standards
- **Handles complex MIME structures** - Multipart messages, nested parts, attachments
- **Security limits** - Built-in protection against deeply nested messages and oversized headers
> [!NOTE]
> Full documentation is available at [postal-mime.postalsys.com](https://postal-mime.postalsys.com/).
## Table of Contents
- [Source](#source)
- [Demo](#demo)
- [Installation](#installation)
- [Usage](#usage)
- [Browser](#browser)
- [Node.js](#nodejs)
- [Cloudflare Email Workers](#cloudflare-email-workers)
- [TypeScript Support](#typescript-support)
- [API](#api)
- [PostalMime.parse()](#postalmimeparse)
- [Utility Functions](#utility-functions)
- [addressParser()](#addressparser)
- [decodeWords()](#decodewords)
- [License](#license)
---
## Source
The source code is available on [GitHub](https://github.com/postalsys/postal-mime).
## Demo
Try out a live demo using the [example page](https://postal-mime.postalsys.com/demo).
## Installation
Install the module from npm:
```bash
npm install postal-mime
```
## Usage
You can import the `PostalMime` class differently depending on your environment:
### Browser
To use PostalMime in the browser (including Web Workers), import it from the `src` folder:
```js
import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
const email = await PostalMime.parse(`Subject: My awesome email 🤓
Content-Type: text/html; charset=utf-8
<p>Hello world 😵‍💫</p>`);
console.log(email.subject); // "My awesome email 🤓"
```
<details>
<summary><strong>TypeScript</strong></summary>
```typescript
import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
import type { Email } from 'postal-mime';
const email: Email = await PostalMime.parse(`Subject: My awesome email 🤓
Content-Type: text/html; charset=utf-8
<p>Hello world 😵‍💫</p>`);
console.log(email.subject); // "My awesome email 🤓"
```
</details>
### Node.js
In Node.js (including serverless functions), import it directly from `postal-mime`:
```js
import PostalMime from 'postal-mime';
import util from 'node:util';
const email = await PostalMime.parse(`Subject: My awesome email 🤓
Content-Type: text/html; charset=utf-8
<p>Hello world 😵‍💫</p>`);
// Use 'util.inspect' for pretty-printing
console.log(util.inspect(email, false, 22, true));
```
<details>
<summary><strong>TypeScript</strong></summary>
```typescript
import PostalMime from 'postal-mime';
import type { Email, PostalMimeOptions } from 'postal-mime';
import util from 'node:util';
const options: PostalMimeOptions = {
attachmentEncoding: 'base64'
};
const email: Email = await PostalMime.parse(`Subject: My awesome email 🤓
Content-Type: text/html; charset=utf-8
<p>Hello world 😵‍💫</p>`, options);
// Use 'util.inspect' for pretty-printing
console.log(util.inspect(email, false, 22, true));
```
</details>
### CommonJS
For projects using CommonJS (with `require()`), postal-mime automatically provides the CommonJS build:
```js
const PostalMime = require('postal-mime');
const { addressParser, decodeWords } = require('postal-mime');
const email = await PostalMime.parse(`Subject: My awesome email 🤓
Content-Type: text/html; charset=utf-8
<p>Hello world 😵‍💫</p>`);
console.log(email.subject); // "My awesome email 🤓"
```
> [!NOTE]
> The CommonJS build is automatically generated from the ESM source code during the build process. The package supports dual module format, so both `import` and `require()` work seamlessly.
### Cloudflare Email Workers
Use the `message.raw` as the raw email data for parsing:
```js
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
console.log('Subject:', email.subject);
console.log('HTML:', email.html);
console.log('Text:', email.text);
}
};
```
<details>
<summary><strong>TypeScript</strong></summary>
```typescript
import PostalMime from 'postal-mime';
import type { Email } from 'postal-mime';
export default {
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
const email: Email = await PostalMime.parse(message.raw);
console.log('Subject:', email.subject);
console.log('HTML:', email.html);
console.log('Text:', email.text);
}
};
```
</details>
---
## TypeScript Support
PostalMime includes comprehensive TypeScript type definitions. All types are exported and can be imported from the main package:
```typescript
import PostalMime, { addressParser, decodeWords } from 'postal-mime';
import type {
Email,
Address,
Mailbox,
Header,
Attachment,
PostalMimeOptions,
AddressParserOptions,
RawEmail
} from 'postal-mime';
```
> [!NOTE]
> PostalMime is written in JavaScript but provides comprehensive TypeScript type definitions. All types are validated through both compile-time type checking and runtime type validation tests to ensure accuracy.
### Available Types
- **`Email`** - The main parsed email object returned by `PostalMime.parse()`
- **`Address`** - Union type representing either a `Mailbox` or an address group
- **`Mailbox`** - Individual email address with name and address fields
- **`Header`** - Email header with key and value
- **`Attachment`** - Email attachment with metadata and content
- **`PostalMimeOptions`** - Configuration options for parsing
- **`AddressParserOptions`** - Configuration options for address parsing
- **`RawEmail`** - Union type for all accepted email input formats
### Type Narrowing
TypeScript users can use type guards to narrow address types:
```typescript
import type { Address, Mailbox } from 'postal-mime';
function isMailbox(addr: Address): addr is Mailbox {
return !('group' in addr) || addr.group === undefined;
}
// Usage
if (email.from && isMailbox(email.from)) {
console.log(email.from.address); // TypeScript knows this is a Mailbox
}
```
---
## API
### PostalMime.parse()
```js
PostalMime.parse(email, options) -> Promise<Email>
```
- **email**: An RFC822 formatted email. This can be a `string`, `ArrayBuffer/Uint8Array`, `Blob`, `Buffer` (Node.js), or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
- **options**: Optional configuration object:
- **rfc822Attachments** (boolean, default: `false`): Treat `message/rfc822` attachments without a Content-Disposition as attachments.
- **forceRfc822Attachments** (boolean, default: `false`): Treat _all_ `message/rfc822` parts as attachments.
- **attachmentEncoding** (string, default: `"arraybuffer"`): Determines how attachment content is decoded in the parsed email:
- `"base64"`
- `"utf8"`
- `"arraybuffer"` (no decoding, returns `ArrayBuffer`)
- **maxNestingDepth** (number, default: `256`): Maximum allowed MIME part nesting depth. Throws an error if exceeded.
- **maxHeadersSize** (number, default: `2097152`): Maximum allowed total header size in bytes (default 2MB). Throws an error if exceeded.
> [!IMPORTANT]
> The `maxNestingDepth` and `maxHeadersSize` options provide built-in security against malicious emails with deeply nested MIME structures or oversized headers that could cause performance issues or memory exhaustion.
**Returns**: A Promise that resolves to a structured `Email` object with the following properties:
- **headers**: An array of `Header` objects, each containing:
- `key`: Lowercase header name (e.g., `"dkim-signature"`).
- `value`: Unprocessed header value as a string.
- **from**, **sender**: Processed `Address` objects (can be a `Mailbox` or address group):
- `name`: Decoded display name, or an empty string if not set.
- `address`: Email address.
- `group`: Array of `Mailbox` objects (only for address groups).
- **deliveredTo**, **returnPath**: Single email addresses as strings.
- **to**, **cc**, **bcc**, **replyTo**: Arrays of `Address` objects (same structure as `from`).
- **subject**: Subject line of the email.
- **messageId**, **inReplyTo**, **references**: Values from their corresponding headers.
- **date**: The email's sending time in ISO 8601 format (or the original string if parsing fails).
- **html**: String containing the HTML content of the email.
- **text**: String containing the plain text content of the email.
- **attachments**: Array of `Attachment` objects:
- `filename`: String or `null`
- `mimeType`: String
- `disposition`: `"attachment"`, `"inline"`, or `null`
- `related`: Boolean (optional, `true` if it's an inline image)
- `contentId`: String (optional)
- `content`: `ArrayBuffer` or string, depending on `attachmentEncoding`
- `encoding`: `"base64"` or `"utf8"` (optional)
<details>
<summary><strong>TypeScript Types</strong></summary>
```typescript
import type {
Email,
Address,
Mailbox,
Header,
Attachment,
PostalMimeOptions,
RawEmail
} from 'postal-mime';
// Main email parsing
const email: Email = await PostalMime.parse(rawEmail);
// With options
const options: PostalMimeOptions = {
attachmentEncoding: 'base64',
maxNestingDepth: 100
};
const email: Email = await PostalMime.parse(rawEmail, options);
// Working with addresses
if (email.from) {
// Address can be either a Mailbox or a Group
if ('group' in email.from && email.from.group) {
// It's a group
email.from.group.forEach((member: Mailbox) => {
console.log(member.address);
});
} else {
// It's a mailbox
const mailbox = email.from as Mailbox;
console.log(mailbox.address);
}
}
// Working with attachments
email.attachments.forEach((att: Attachment) => {
if (att.encoding === 'base64') {
// content is a string
const base64Content: string = att.content as string;
} else {
// content is ArrayBuffer (default)
const buffer: ArrayBuffer = att.content as ArrayBuffer;
}
});
```
</details>
---
### Utility Functions
#### addressParser()
```js
import { addressParser } from 'postal-mime';
addressParser(addressStr, opts) -> Address[]
```
- **addressStr**: A raw address header string.
- **opts**: Optional configuration:
- **flatten** (boolean, default: `false`): If `true`, ignores address groups and returns a flat array of addresses.
**Returns**: An array of `Address` objects, which can be nested if address groups are present.
**Example**:
```js
import { addressParser } from 'postal-mime';
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
console.log(addressParser(addressStr));
// [ { name: 'エポスカード', address: 'support@example.com' } ]
```
<details>
<summary><strong>TypeScript</strong></summary>
```typescript
import { addressParser } from 'postal-mime';
import type { Address, AddressParserOptions } from 'postal-mime';
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
const addresses: Address[] = addressParser(addressStr);
// With options
const options: AddressParserOptions = { flatten: true };
const flatAddresses: Address[] = addressParser(addressStr, options);
```
</details>
#### decodeWords()
```js
import { decodeWords } from 'postal-mime';
decodeWords(encodedStr) -> string
```
- **encodedStr**: A string that may contain MIME encoded-words.
**Returns**: A Unicode string with all encoded-words decoded.
**Example**:
```js
import { decodeWords } from 'postal-mime';
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
console.log(decodeWords(encodedStr));
// Hello, エポスカード
```
<details>
<summary><strong>TypeScript</strong></summary>
```typescript
import { decodeWords } from 'postal-mime';
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
const decoded: string = decodeWords(encodedStr);
console.log(decoded); // Hello, エポスカード
```
</details>
---
## License
&copy; 20212026 Andris Reinman
`postal-mime` is licensed under the **MIT No Attribution license**.

325
node_modules/postal-mime/dist/address-parser.cjs generated vendored Normal file
View File

@@ -0,0 +1,325 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var address_parser_exports = {};
__export(address_parser_exports, {
default: () => address_parser_default
});
module.exports = __toCommonJS(address_parser_exports);
var import_decode_strings = require("./decode-strings.cjs");
function _handleAddress(tokens, depth) {
let isGroup = false;
let state = "text";
let address;
let addresses = [];
let data = {
address: [],
comment: [],
group: [],
text: [],
textWasQuoted: []
// Track which text tokens came from inside quotes
};
let i;
let len;
let insideQuotes = false;
for (i = 0, len = tokens.length; i < len; i++) {
let token = tokens[i];
let prevToken = i ? tokens[i - 1] : null;
if (token.type === "operator") {
switch (token.value) {
case "<":
state = "address";
insideQuotes = false;
break;
case "(":
state = "comment";
insideQuotes = false;
break;
case ":":
state = "group";
isGroup = true;
insideQuotes = false;
break;
case '"':
insideQuotes = !insideQuotes;
state = "text";
break;
default:
state = "text";
insideQuotes = false;
break;
}
} else if (token.value) {
if (state === "address") {
token.value = token.value.replace(/^[^<]*<\s*/, "");
}
if (prevToken && prevToken.noBreak && data[state].length) {
data[state][data[state].length - 1] += token.value;
if (state === "text" && insideQuotes) {
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
}
} else {
data[state].push(token.value);
if (state === "text") {
data.textWasQuoted.push(insideQuotes);
}
}
}
}
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
data.text = data.text.join(" ");
let groupMembers = [];
if (data.group.length) {
let parsedGroup = addressParser(data.group.join(","), { _depth: depth + 1 });
parsedGroup.forEach((member) => {
if (member.group) {
groupMembers = groupMembers.concat(member.group);
} else {
groupMembers.push(member);
}
});
}
addresses.push({
name: (0, import_decode_strings.decodeWords)(data.text || address && address.name),
group: groupMembers
});
} else {
if (!data.address.length && data.text.length) {
for (i = data.text.length - 1; i >= 0; i--) {
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
data.textWasQuoted.splice(i, 1);
break;
}
}
let _regexHandler = function(address2) {
if (!data.address.length) {
data.address = [address2.trim()];
return " ";
} else {
return address2;
}
};
if (!data.address.length) {
for (i = data.text.length - 1; i >= 0; i--) {
if (!data.textWasQuoted[i]) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
if (data.address.length) {
break;
}
}
}
}
}
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
data.text = data.text.join(" ");
data.address = data.address.join(" ");
if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
const decodedText = (0, import_decode_strings.decodeWords)(data.text);
if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
const parsedSubAddresses = addressParser(decodedText);
if (parsedSubAddresses && parsedSubAddresses.length) {
return parsedSubAddresses;
}
}
return [{ address: "", name: decodedText }];
}
address = {
address: data.address || data.text || "",
name: (0, import_decode_strings.decodeWords)(data.text || data.address || "")
};
if (address.address === address.name) {
if ((address.address || "").match(/@/)) {
address.name = "";
} else {
address.address = "";
}
}
addresses.push(address);
}
return addresses;
}
class Tokenizer {
constructor(str) {
this.str = (str || "").toString();
this.operatorCurrent = "";
this.operatorExpecting = "";
this.node = null;
this.escaped = false;
this.list = [];
this.operators = {
'"': '"',
"(": ")",
"<": ">",
",": "",
":": ";",
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
";": ""
};
}
/**
* Tokenizes the original input string
*
* @return {Array} An array of operator|text tokens
*/
tokenize() {
let list = [];
for (let i = 0, len = this.str.length; i < len; i++) {
let chr = this.str.charAt(i);
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
this.checkChar(chr, nextChr);
}
this.list.forEach((node) => {
node.value = (node.value || "").toString().trim();
if (node.value) {
list.push(node);
}
});
return list;
}
/**
* Checks if a character is an operator or text and acts accordingly
*
* @param {String} chr Character from the address field
*/
checkChar(chr, nextChr) {
if (this.escaped) {
} else if (chr === this.operatorExpecting) {
this.node = {
type: "operator",
value: chr
};
if (nextChr && ![" ", " ", "\r", "\n", ",", ";"].includes(nextChr)) {
this.node.noBreak = true;
}
this.list.push(this.node);
this.node = null;
this.operatorExpecting = "";
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in this.operators) {
this.node = {
type: "operator",
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = this.operators[chr];
this.escaped = false;
return;
} else if (this.operatorExpecting === '"' && chr === "\\") {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: "text",
value: ""
};
this.list.push(this.node);
}
if (chr === "\n") {
chr = " ";
}
if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
this.node.value += chr;
}
this.escaped = false;
}
}
const MAX_NESTED_GROUP_DEPTH = 50;
function addressParser(str, options) {
options = options || {};
let depth = options._depth || 0;
if (depth > MAX_NESTED_GROUP_DEPTH) {
return [];
}
let tokenizer = new Tokenizer(str);
let tokens = tokenizer.tokenize();
let addresses = [];
let address = [];
let parsedAddresses = [];
tokens.forEach((token) => {
if (token.type === "operator" && (token.value === "," || token.value === ";")) {
if (address.length) {
addresses.push(address);
}
address = [];
} else {
address.push(token);
}
});
if (address.length) {
addresses.push(address);
}
addresses.forEach((address2) => {
address2 = _handleAddress(address2, depth);
if (address2.length) {
parsedAddresses = parsedAddresses.concat(address2);
}
});
if (options.flatten) {
let addresses2 = [];
let walkAddressList = (list) => {
list.forEach((address2) => {
if (address2.group) {
return walkAddressList(address2.group);
} else {
addresses2.push(address2);
}
});
};
walkAddressList(parsedAddresses);
return addresses2;
}
return parsedAddresses;
}
var address_parser_default = addressParser;
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

75
node_modules/postal-mime/dist/base64-decoder.cjs generated vendored Normal file
View File

@@ -0,0 +1,75 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var base64_decoder_exports = {};
__export(base64_decoder_exports, {
default: () => Base64Decoder
});
module.exports = __toCommonJS(base64_decoder_exports);
var import_decode_strings = require("./decode-strings.cjs");
class Base64Decoder {
constructor(opts) {
opts = opts || {};
this.decoder = opts.decoder || new TextDecoder();
this.maxChunkSize = 100 * 1024;
this.chunks = [];
this.remainder = "";
}
update(buffer) {
let str = this.decoder.decode(buffer);
str = str.replace(/[^a-zA-Z0-9+\/]+/g, "");
this.remainder += str;
if (this.remainder.length >= this.maxChunkSize) {
let allowedBytes = Math.floor(this.remainder.length / 4) * 4;
let base64Str;
if (allowedBytes === this.remainder.length) {
base64Str = this.remainder;
this.remainder = "";
} else {
base64Str = this.remainder.substr(0, allowedBytes);
this.remainder = this.remainder.substr(allowedBytes);
}
if (base64Str.length) {
this.chunks.push((0, import_decode_strings.decodeBase64)(base64Str));
}
}
}
finalize() {
if (this.remainder && !/^=+$/.test(this.remainder)) {
this.chunks.push((0, import_decode_strings.decodeBase64)(this.remainder));
}
return (0, import_decode_strings.blobToArrayBuffer)(new Blob(this.chunks, { type: "application/octet-stream" }));
}
}
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

75
node_modules/postal-mime/dist/base64-encoder.cjs generated vendored Normal file
View File

@@ -0,0 +1,75 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var base64_encoder_exports = {};
__export(base64_encoder_exports, {
base64ArrayBuffer: () => base64ArrayBuffer
});
module.exports = __toCommonJS(base64_encoder_exports);
function base64ArrayBuffer(arrayBuffer) {
var base64 = "";
var encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var bytes = new Uint8Array(arrayBuffer);
var byteLength = bytes.byteLength;
var byteRemainder = byteLength % 3;
var mainLength = byteLength - byteRemainder;
var a, b, c, d;
var chunk;
for (var i = 0; i < mainLength; i = i + 3) {
chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
a = (chunk & 16515072) >> 18;
b = (chunk & 258048) >> 12;
c = (chunk & 4032) >> 6;
d = chunk & 63;
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
if (byteRemainder == 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2;
b = (chunk & 3) << 4;
base64 += encodings[a] + encodings[b] + "==";
} else if (byteRemainder == 2) {
chunk = bytes[mainLength] << 8 | bytes[mainLength + 1];
a = (chunk & 64512) >> 10;
b = (chunk & 1008) >> 4;
c = (chunk & 15) << 2;
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
}
return base64;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
base64ArrayBuffer
});
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

257
node_modules/postal-mime/dist/decode-strings.cjs generated vendored Normal file
View File

@@ -0,0 +1,257 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var decode_strings_exports = {};
__export(decode_strings_exports, {
blobToArrayBuffer: () => blobToArrayBuffer,
decodeBase64: () => decodeBase64,
decodeParameterValueContinuations: () => decodeParameterValueContinuations,
decodeURIComponentWithCharset: () => decodeURIComponentWithCharset,
decodeWord: () => decodeWord,
decodeWords: () => decodeWords,
getDecoder: () => getDecoder,
getHex: () => getHex,
textEncoder: () => textEncoder
});
module.exports = __toCommonJS(decode_strings_exports);
const textEncoder = new TextEncoder();
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const base64Lookup = new Uint8Array(256);
for (let i = 0; i < base64Chars.length; i++) {
base64Lookup[base64Chars.charCodeAt(i)] = i;
}
function decodeBase64(base64) {
let bufferLength = Math.ceil(base64.length / 4) * 3;
const len = base64.length;
let p = 0;
if (base64.length % 4 === 3) {
bufferLength--;
} else if (base64.length % 4 === 2) {
bufferLength -= 2;
} else if (base64[base64.length - 1] === "=") {
bufferLength--;
if (base64[base64.length - 2] === "=") {
bufferLength--;
}
}
const arrayBuffer = new ArrayBuffer(bufferLength);
const bytes = new Uint8Array(arrayBuffer);
for (let i = 0; i < len; i += 4) {
let encoded1 = base64Lookup[base64.charCodeAt(i)];
let encoded2 = base64Lookup[base64.charCodeAt(i + 1)];
let encoded3 = base64Lookup[base64.charCodeAt(i + 2)];
let encoded4 = base64Lookup[base64.charCodeAt(i + 3)];
bytes[p++] = encoded1 << 2 | encoded2 >> 4;
bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2;
bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63;
}
return arrayBuffer;
}
function getDecoder(charset) {
charset = charset || "utf8";
let decoder;
try {
decoder = new TextDecoder(charset);
} catch (err) {
decoder = new TextDecoder("windows-1252");
}
return decoder;
}
async function blobToArrayBuffer(blob) {
if ("arrayBuffer" in blob) {
return await blob.arrayBuffer();
}
const fr = new FileReader();
return new Promise((resolve, reject) => {
fr.onload = function(e) {
resolve(e.target.result);
};
fr.onerror = function(e) {
reject(fr.error);
};
fr.readAsArrayBuffer(blob);
});
}
function getHex(c) {
if (c >= 48 && c <= 57 || c >= 97 && c <= 102 || c >= 65 && c <= 70) {
return String.fromCharCode(c);
}
return false;
}
function decodeWord(charset, encoding, str) {
let splitPos = charset.indexOf("*");
if (splitPos >= 0) {
charset = charset.substr(0, splitPos);
}
encoding = encoding.toUpperCase();
let byteStr;
if (encoding === "Q") {
str = str.replace(/=\s+([0-9a-fA-F])/g, "=$1").replace(/[_\s]/g, " ");
let buf = textEncoder.encode(str);
let encodedBytes = [];
for (let i = 0, len = buf.length; i < len; i++) {
let c = buf[i];
if (i <= len - 2 && c === 61) {
let c1 = getHex(buf[i + 1]);
let c2 = getHex(buf[i + 2]);
if (c1 && c2) {
let c3 = parseInt(c1 + c2, 16);
encodedBytes.push(c3);
i += 2;
continue;
}
}
encodedBytes.push(c);
}
byteStr = new ArrayBuffer(encodedBytes.length);
let dataView = new DataView(byteStr);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, encodedBytes[i]);
}
} else if (encoding === "B") {
byteStr = decodeBase64(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ""));
} else {
byteStr = textEncoder.encode(str);
}
return getDecoder(charset).decode(byteStr);
}
function decodeWords(str) {
let joinString = true;
let done = false;
while (!done) {
let result = (str || "").toString().replace(
/(=\?([^?]+)\?[Bb]\?([^?]*)\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g,
(match, left, chLeft, encodedLeftStr, chRight) => {
if (!joinString) {
return match;
}
if (chLeft === chRight && encodedLeftStr.length % 4 === 0 && !/=$/.test(encodedLeftStr)) {
return left + "__\0JOIN\0__";
}
return match;
}
).replace(
/(=\?([^?]+)\?[Qq]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Qq]\?[^?]*\?=)/g,
(match, left, chLeft, chRight) => {
if (!joinString) {
return match;
}
if (chLeft === chRight) {
return left + "__\0JOIN\0__";
}
return match;
}
).replace(/(\?=)?__\x00JOIN\x00__(=\?([^?]+)\?[QqBb]\?)?/g, "").replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, "$1").replace(
/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g,
(m, charset, encoding, text) => decodeWord(charset, encoding, text)
);
if (joinString && result.indexOf("\uFFFD") >= 0) {
joinString = false;
} else {
return result;
}
}
}
function decodeURIComponentWithCharset(encodedStr, charset) {
charset = charset || "utf-8";
let encodedBytes = [];
for (let i = 0; i < encodedStr.length; i++) {
let c = encodedStr.charAt(i);
if (c === "%" && /^[a-f0-9]{2}/i.test(encodedStr.substr(i + 1, 2))) {
let byte = encodedStr.substr(i + 1, 2);
i += 2;
encodedBytes.push(parseInt(byte, 16));
} else if (c.charCodeAt(0) > 126) {
c = textEncoder.encode(c);
for (let j = 0; j < c.length; j++) {
encodedBytes.push(c[j]);
}
} else {
encodedBytes.push(c.charCodeAt(0));
}
}
const byteStr = new ArrayBuffer(encodedBytes.length);
const dataView = new DataView(byteStr);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, encodedBytes[i]);
}
return getDecoder(charset).decode(byteStr);
}
function decodeParameterValueContinuations(header) {
let paramKeys = /* @__PURE__ */ new Map();
Object.keys(header.params).forEach((key) => {
let match = key.match(/\*((\d+)\*?)?$/);
if (!match) {
return;
}
let actualKey = key.substr(0, match.index).toLowerCase();
let nr = Number(match[2]) || 0;
let paramVal;
if (!paramKeys.has(actualKey)) {
paramVal = {
charset: false,
values: []
};
paramKeys.set(actualKey, paramVal);
} else {
paramVal = paramKeys.get(actualKey);
}
let value = header.params[key];
if (nr === 0 && match[0].charAt(match[0].length - 1) === "*" && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
paramVal.charset = match[1] || "utf-8";
value = match[2];
}
paramVal.values.push({ nr, value });
delete header.params[key];
});
paramKeys.forEach((paramVal, key) => {
header.params[key] = decodeURIComponentWithCharset(
paramVal.values.sort((a, b) => a.nr - b.nr).map((a) => a.value).join(""),
paramVal.charset
);
});
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
blobToArrayBuffer,
decodeBase64,
decodeParameterValueContinuations,
decodeURIComponentWithCharset,
decodeWord,
decodeWords,
getDecoder,
getHex,
textEncoder
});
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

2279
node_modules/postal-mime/dist/html-entities.cjs generated vendored Normal file

File diff suppressed because it is too large Load Diff

327
node_modules/postal-mime/dist/mime-node.cjs generated vendored Normal file
View File

@@ -0,0 +1,327 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var mime_node_exports = {};
__export(mime_node_exports, {
default: () => MimeNode
});
module.exports = __toCommonJS(mime_node_exports);
var import_decode_strings = require("./decode-strings.cjs");
var import_pass_through_decoder = __toESM(require("./pass-through-decoder.cjs"), 1);
var import_base64_decoder = __toESM(require("./base64-decoder.cjs"), 1);
var import_qp_decoder = __toESM(require("./qp-decoder.cjs"), 1);
const defaultDecoder = (0, import_decode_strings.getDecoder)();
class MimeNode {
constructor(options) {
this.options = options || {};
this.postalMime = this.options.postalMime;
this.root = !!this.options.parentNode;
this.childNodes = [];
if (this.options.parentNode) {
this.parentNode = this.options.parentNode;
this.depth = this.parentNode.depth + 1;
if (this.depth > this.options.maxNestingDepth) {
throw new Error(`Maximum MIME nesting depth of ${this.options.maxNestingDepth} levels exceeded`);
}
this.options.parentNode.childNodes.push(this);
} else {
this.depth = 0;
}
this.state = "header";
this.headerLines = [];
this.headerSize = 0;
const parentMultipartType = this.options.parentMultipartType || null;
const defaultContentType = parentMultipartType === "digest" ? "message/rfc822" : "text/plain";
this.contentType = {
value: defaultContentType,
default: true
};
this.contentTransferEncoding = {
value: "8bit"
};
this.contentDisposition = {
value: ""
};
this.headers = [];
this.contentDecoder = false;
}
setupContentDecoder(transferEncoding) {
if (/base64/i.test(transferEncoding)) {
this.contentDecoder = new import_base64_decoder.default();
} else if (/quoted-printable/i.test(transferEncoding)) {
this.contentDecoder = new import_qp_decoder.default({ decoder: (0, import_decode_strings.getDecoder)(this.contentType.parsed.params.charset) });
} else {
this.contentDecoder = new import_pass_through_decoder.default();
}
}
async finalize() {
if (this.state === "finished") {
return;
}
if (this.state === "header") {
this.processHeaders();
}
let boundaries = this.postalMime.boundaries;
for (let i = boundaries.length - 1; i >= 0; i--) {
let boundary = boundaries[i];
if (boundary.node === this) {
boundaries.splice(i, 1);
break;
}
}
await this.finalizeChildNodes();
this.content = this.contentDecoder ? await this.contentDecoder.finalize() : null;
this.state = "finished";
}
async finalizeChildNodes() {
for (let childNode of this.childNodes) {
await childNode.finalize();
}
}
// Strip RFC 822 comments (parenthesized text) from structured header values
stripComments(str) {
let result = "";
let depth = 0;
let escaped = false;
let inQuote = false;
for (let i = 0; i < str.length; i++) {
const chr = str.charAt(i);
if (escaped) {
if (depth === 0) {
result += chr;
}
escaped = false;
continue;
}
if (chr === "\\") {
escaped = true;
if (depth === 0) {
result += chr;
}
continue;
}
if (chr === '"' && depth === 0) {
inQuote = !inQuote;
result += chr;
continue;
}
if (!inQuote) {
if (chr === "(") {
depth++;
continue;
}
if (chr === ")" && depth > 0) {
depth--;
continue;
}
}
if (depth === 0) {
result += chr;
}
}
return result;
}
parseStructuredHeader(str) {
str = this.stripComments(str);
let response = {
value: false,
params: {}
};
let key = false;
let value = "";
let stage = "value";
let quote = false;
let escaped = false;
let chr;
for (let i = 0, len = str.length; i < len; i++) {
chr = str.charAt(i);
switch (stage) {
case "key":
if (chr === "=") {
key = value.trim().toLowerCase();
stage = "value";
value = "";
break;
}
value += chr;
break;
case "value":
if (escaped) {
value += chr;
} else if (chr === "\\") {
escaped = true;
continue;
} else if (quote && chr === quote) {
quote = false;
} else if (!quote && chr === '"') {
quote = chr;
} else if (!quote && chr === ";") {
if (key === false) {
response.value = value.trim();
} else {
response.params[key] = value.trim();
}
stage = "key";
value = "";
} else {
value += chr;
}
escaped = false;
break;
}
}
value = value.trim();
if (stage === "value") {
if (key === false) {
response.value = value;
} else {
response.params[key] = value;
}
} else if (value) {
response.params[value.toLowerCase()] = "";
}
if (response.value) {
response.value = response.value.toLowerCase();
}
(0, import_decode_strings.decodeParameterValueContinuations)(response);
return response;
}
decodeFlowedText(str, delSp) {
return str.split(/\r?\n/).reduce((previousValue, currentValue) => {
if (previousValue.endsWith(" ") && previousValue !== "-- " && !previousValue.endsWith("\n-- ")) {
if (delSp) {
return previousValue.slice(0, -1) + currentValue;
} else {
return previousValue + currentValue;
}
} else {
return previousValue + "\n" + currentValue;
}
}).replace(/^ /gm, "");
}
getTextContent() {
if (!this.content) {
return "";
}
let str = (0, import_decode_strings.getDecoder)(this.contentType.parsed.params.charset).decode(this.content);
if (/^flowed$/i.test(this.contentType.parsed.params.format)) {
str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp));
}
return str;
}
processHeaders() {
for (let i = this.headerLines.length - 1; i >= 0; i--) {
let line = this.headerLines[i];
if (i && /^\s/.test(line)) {
this.headerLines[i - 1] += "\n" + line;
this.headerLines.splice(i, 1);
}
}
this.rawHeaderLines = [];
for (let i = this.headerLines.length - 1; i >= 0; i--) {
let rawLine = this.headerLines[i];
let sep = rawLine.indexOf(":");
let rawKey = sep < 0 ? rawLine.trim() : rawLine.substr(0, sep).trim();
this.rawHeaderLines.push({
key: rawKey.toLowerCase(),
line: rawLine
});
let normalizedLine = rawLine.replace(/\s+/g, " ");
sep = normalizedLine.indexOf(":");
let key = sep < 0 ? normalizedLine.trim() : normalizedLine.substr(0, sep).trim();
let value = sep < 0 ? "" : normalizedLine.substr(sep + 1).trim();
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
switch (key.toLowerCase()) {
case "content-type":
if (this.contentType.default) {
this.contentType = { value, parsed: {} };
}
break;
case "content-transfer-encoding":
this.contentTransferEncoding = { value, parsed: {} };
break;
case "content-disposition":
this.contentDisposition = { value, parsed: {} };
break;
case "content-id":
this.contentId = value;
break;
case "content-description":
this.contentDescription = value;
break;
}
}
this.contentType.parsed = this.parseStructuredHeader(this.contentType.value);
this.contentType.multipart = /^multipart\//i.test(this.contentType.parsed.value) ? this.contentType.parsed.value.substr(this.contentType.parsed.value.indexOf("/") + 1) : false;
if (this.contentType.multipart && this.contentType.parsed.params.boundary) {
this.postalMime.boundaries.push({
value: import_decode_strings.textEncoder.encode(this.contentType.parsed.params.boundary),
node: this
});
}
this.contentDisposition.parsed = this.parseStructuredHeader(this.contentDisposition.value);
this.contentTransferEncoding.encoding = this.contentTransferEncoding.value.toLowerCase().split(/[^\w-]/).shift();
this.setupContentDecoder(this.contentTransferEncoding.encoding);
}
feed(line) {
switch (this.state) {
case "header":
if (!line.length) {
this.state = "body";
return this.processHeaders();
}
this.headerSize += line.length;
if (this.headerSize > this.options.maxHeadersSize) {
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
throw error;
}
this.headerLines.push(defaultDecoder.decode(line));
break;
case "body": {
this.contentDecoder.update(line);
}
}
}
}
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

53
node_modules/postal-mime/dist/pass-through-decoder.cjs generated vendored Normal file
View File

@@ -0,0 +1,53 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var pass_through_decoder_exports = {};
__export(pass_through_decoder_exports, {
default: () => PassThroughDecoder
});
module.exports = __toCommonJS(pass_through_decoder_exports);
var import_decode_strings = require("./decode-strings.cjs");
class PassThroughDecoder {
constructor() {
this.chunks = [];
}
update(line) {
this.chunks.push(line);
this.chunks.push("\n");
}
finalize() {
return (0, import_decode_strings.blobToArrayBuffer)(new Blob(this.chunks, { type: "application/octet-stream" }));
}
}
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

496
node_modules/postal-mime/dist/postal-mime.cjs generated vendored Normal file
View File

@@ -0,0 +1,496 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var postal_mime_exports = {};
__export(postal_mime_exports, {
addressParser: () => import_address_parser.default,
decodeWords: () => import_decode_strings.decodeWords,
default: () => PostalMime
});
module.exports = __toCommonJS(postal_mime_exports);
var import_mime_node = __toESM(require("./mime-node.cjs"), 1);
var import_text_format = require("./text-format.cjs");
var import_address_parser = __toESM(require("./address-parser.cjs"), 1);
var import_decode_strings = require("./decode-strings.cjs");
var import_base64_encoder = require("./base64-encoder.cjs");
const MAX_NESTING_DEPTH = 256;
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
function toCamelCase(key) {
return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
}
class PostalMime {
static parse(buf, options) {
const parser = new PostalMime(options);
return parser.parse(buf);
}
constructor(options) {
this.options = options || {};
this.mimeOptions = {
maxNestingDepth: this.options.maxNestingDepth || MAX_NESTING_DEPTH,
maxHeadersSize: this.options.maxHeadersSize || MAX_HEADERS_SIZE
};
this.root = this.currentNode = new import_mime_node.default({
postalMime: this,
...this.mimeOptions
});
this.boundaries = [];
this.textContent = {};
this.attachments = [];
this.attachmentEncoding = (this.options.attachmentEncoding || "").toString().replace(/[-_\s]/g, "").trim().toLowerCase() || "arraybuffer";
this.started = false;
}
async finalize() {
await this.root.finalize();
}
async processLine(line, isFinal) {
let boundaries = this.boundaries;
if (boundaries.length && line.length > 2 && line[0] === 45 && line[1] === 45) {
for (let i = boundaries.length - 1; i >= 0; i--) {
let boundary = boundaries[i];
if (line.length < boundary.value.length + 2) {
continue;
}
let boundaryMatches = true;
for (let j = 0; j < boundary.value.length; j++) {
if (line[j + 2] !== boundary.value[j]) {
boundaryMatches = false;
break;
}
}
if (!boundaryMatches) {
continue;
}
let boundaryEnd = boundary.value.length + 2;
let isTerminator = false;
if (line.length >= boundary.value.length + 4 && line[boundary.value.length + 2] === 45 && line[boundary.value.length + 3] === 45) {
isTerminator = true;
boundaryEnd = boundary.value.length + 4;
}
let hasValidTrailing = true;
for (let j = boundaryEnd; j < line.length; j++) {
if (line[j] !== 32 && line[j] !== 9) {
hasValidTrailing = false;
break;
}
}
if (!hasValidTrailing) {
continue;
}
if (isTerminator) {
await boundary.node.finalize();
this.currentNode = boundary.node.parentNode || this.root;
} else {
await boundary.node.finalizeChildNodes();
this.currentNode = new import_mime_node.default({
postalMime: this,
parentNode: boundary.node,
parentMultipartType: boundary.node.contentType.multipart,
...this.mimeOptions
});
}
if (isFinal) {
return this.finalize();
}
return;
}
}
this.currentNode.feed(line);
if (isFinal) {
return this.finalize();
}
}
readLine() {
let startPos = this.readPos;
let endPos = this.readPos;
while (this.readPos < this.av.length) {
const c = this.av[this.readPos++];
if (c !== 13 && c !== 10) {
endPos = this.readPos;
}
if (c === 10) {
return {
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
done: this.readPos >= this.av.length
};
}
}
return {
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
done: this.readPos >= this.av.length
};
}
async processNodeTree() {
let textContent = {};
let textTypes = /* @__PURE__ */ new Set();
let textMap = this.textMap = /* @__PURE__ */ new Map();
let forceRfc822Attachments = this.forceRfc822Attachments();
let walk = async (node, alternative, related) => {
var _a, _b, _c, _d, _e;
alternative = alternative || false;
related = related || false;
if (!node.contentType.multipart) {
if (this.isInlineMessageRfc822(node) && !forceRfc822Attachments) {
const subParser = new PostalMime();
node.subMessage = await subParser.parse(node.content);
if (!textMap.has(node)) {
textMap.set(node, {});
}
let textEntry = textMap.get(node);
if (node.subMessage.text || !node.subMessage.html) {
textEntry.plain = textEntry.plain || [];
textEntry.plain.push({ type: "subMessage", value: node.subMessage });
textTypes.add("plain");
}
if (node.subMessage.html) {
textEntry.html = textEntry.html || [];
textEntry.html.push({ type: "subMessage", value: node.subMessage });
textTypes.add("html");
}
if (subParser.textMap) {
subParser.textMap.forEach((subTextEntry, subTextNode) => {
textMap.set(subTextNode, subTextEntry);
});
}
for (let attachment of node.subMessage.attachments || []) {
this.attachments.push(attachment);
}
} else if (this.isInlineTextNode(node)) {
let textType = node.contentType.parsed.value.substr(node.contentType.parsed.value.indexOf("/") + 1);
let selectorNode = alternative || node;
if (!textMap.has(selectorNode)) {
textMap.set(selectorNode, {});
}
let textEntry = textMap.get(selectorNode);
textEntry[textType] = textEntry[textType] || [];
textEntry[textType].push({ type: "text", value: node.getTextContent() });
textTypes.add(textType);
} else if (node.content) {
const filename = ((_c = (_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.params) == null ? void 0 : _c.filename) || node.contentType.parsed.params.name || null;
const attachment = {
filename: filename ? (0, import_decode_strings.decodeWords)(filename) : null,
mimeType: node.contentType.parsed.value,
disposition: ((_e = (_d = node.contentDisposition) == null ? void 0 : _d.parsed) == null ? void 0 : _e.value) || null
};
if (related && node.contentId) {
attachment.related = true;
}
if (node.contentDescription) {
attachment.description = node.contentDescription;
}
if (node.contentId) {
attachment.contentId = node.contentId;
}
switch (node.contentType.parsed.value) {
// Special handling for calendar events
case "text/calendar":
case "application/ics": {
if (node.contentType.parsed.params.method) {
attachment.method = node.contentType.parsed.params.method.toString().toUpperCase().trim();
}
const decodedText = node.getTextContent().replace(/\r?\n/g, "\n").replace(/\n*$/, "\n");
attachment.content = import_decode_strings.textEncoder.encode(decodedText);
break;
}
// Regular attachments
default:
attachment.content = node.content;
}
this.attachments.push(attachment);
}
} else if (node.contentType.multipart === "alternative") {
alternative = node;
} else if (node.contentType.multipart === "related") {
related = node;
}
for (let childNode of node.childNodes) {
await walk(childNode, alternative, related);
}
};
await walk(this.root, false, false);
textMap.forEach((mapEntry) => {
textTypes.forEach((textType) => {
if (!textContent[textType]) {
textContent[textType] = [];
}
if (mapEntry[textType]) {
mapEntry[textType].forEach((textEntry) => {
switch (textEntry.type) {
case "text":
textContent[textType].push(textEntry.value);
break;
case "subMessage":
{
switch (textType) {
case "html":
textContent[textType].push((0, import_text_format.formatHtmlHeader)(textEntry.value));
break;
case "plain":
textContent[textType].push((0, import_text_format.formatTextHeader)(textEntry.value));
break;
}
}
break;
}
});
} else {
let alternativeType;
switch (textType) {
case "html":
alternativeType = "plain";
break;
case "plain":
alternativeType = "html";
break;
}
(mapEntry[alternativeType] || []).forEach((textEntry) => {
switch (textEntry.type) {
case "text":
switch (textType) {
case "html":
textContent[textType].push((0, import_text_format.textToHtml)(textEntry.value));
break;
case "plain":
textContent[textType].push((0, import_text_format.htmlToText)(textEntry.value));
break;
}
break;
case "subMessage":
{
switch (textType) {
case "html":
textContent[textType].push((0, import_text_format.formatHtmlHeader)(textEntry.value));
break;
case "plain":
textContent[textType].push((0, import_text_format.formatTextHeader)(textEntry.value));
break;
}
}
break;
}
});
}
});
});
Object.keys(textContent).forEach((textType) => {
textContent[textType] = textContent[textType].join("\n");
});
this.textContent = textContent;
}
isInlineTextNode(node) {
var _a, _b, _c;
if (((_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.value) === "attachment") {
return false;
}
switch ((_c = node.contentType.parsed) == null ? void 0 : _c.value) {
case "text/html":
case "text/plain":
return true;
case "text/calendar":
case "text/csv":
default:
return false;
}
}
isInlineMessageRfc822(node) {
var _a, _b, _c;
if (((_a = node.contentType.parsed) == null ? void 0 : _a.value) !== "message/rfc822") {
return false;
}
let disposition = ((_c = (_b = node.contentDisposition) == null ? void 0 : _b.parsed) == null ? void 0 : _c.value) || (this.options.rfc822Attachments ? "attachment" : "inline");
return disposition === "inline";
}
// Check if this is a specially crafted report email where message/rfc822 content should not be inlined
forceRfc822Attachments() {
if (this.options.forceRfc822Attachments) {
return true;
}
let forceRfc822Attachments = false;
let walk = (node) => {
if (!node.contentType.multipart) {
if (node.contentType.parsed && ["message/delivery-status", "message/feedback-report"].includes(node.contentType.parsed.value)) {
forceRfc822Attachments = true;
}
}
for (let childNode of node.childNodes) {
walk(childNode);
}
};
walk(this.root);
return forceRfc822Attachments;
}
async resolveStream(stream) {
let chunkLen = 0;
let chunks = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
chunkLen += value.length;
}
const result = new Uint8Array(chunkLen);
let chunkPointer = 0;
for (let chunk of chunks) {
result.set(chunk, chunkPointer);
chunkPointer += chunk.length;
}
return result;
}
async parse(buf) {
var _a, _b;
if (this.started) {
throw new Error("Can not reuse parser, create a new PostalMime object");
}
this.started = true;
if (buf && typeof buf.getReader === "function") {
buf = await this.resolveStream(buf);
}
buf = buf || new ArrayBuffer(0);
if (typeof buf === "string") {
buf = import_decode_strings.textEncoder.encode(buf);
}
if (buf instanceof Blob || Object.prototype.toString.call(buf) === "[object Blob]") {
buf = await (0, import_decode_strings.blobToArrayBuffer)(buf);
}
if (buf.buffer instanceof ArrayBuffer) {
buf = new Uint8Array(buf).buffer;
}
this.buf = buf;
this.av = new Uint8Array(buf);
this.readPos = 0;
while (this.readPos < this.av.length) {
const line = this.readLine();
await this.processLine(line.bytes, line.done);
}
await this.processNodeTree();
const message = {
headers: this.root.headers.map((entry) => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value })).reverse()
};
for (const key of ["from", "sender"]) {
const addressHeader = this.root.headers.find((line) => line.key === key);
if (addressHeader && addressHeader.value) {
const addresses = (0, import_address_parser.default)(addressHeader.value);
if (addresses && addresses.length) {
message[key] = addresses[0];
}
}
}
for (const key of ["delivered-to", "return-path"]) {
const addressHeader = this.root.headers.find((line) => line.key === key);
if (addressHeader && addressHeader.value) {
const addresses = (0, import_address_parser.default)(addressHeader.value);
if (addresses && addresses.length && addresses[0].address) {
const camelKey = toCamelCase(key);
message[camelKey] = addresses[0].address;
}
}
}
for (const key of ["to", "cc", "bcc", "reply-to"]) {
const addressHeaders = this.root.headers.filter((line) => line.key === key);
let addresses = [];
addressHeaders.filter((entry) => entry && entry.value).map((entry) => (0, import_address_parser.default)(entry.value)).forEach((parsed) => addresses = addresses.concat(parsed || []));
if (addresses && addresses.length) {
const camelKey = toCamelCase(key);
message[camelKey] = addresses;
}
}
for (const key of ["subject", "message-id", "in-reply-to", "references"]) {
const header = this.root.headers.find((line) => line.key === key);
if (header && header.value) {
const camelKey = toCamelCase(key);
message[camelKey] = (0, import_decode_strings.decodeWords)(header.value);
}
}
let dateHeader = this.root.headers.find((line) => line.key === "date");
if (dateHeader) {
let date = new Date(dateHeader.value);
if (date.toString() === "Invalid Date") {
date = dateHeader.value;
} else {
date = date.toISOString();
}
message.date = date;
}
if ((_a = this.textContent) == null ? void 0 : _a.html) {
message.html = this.textContent.html;
}
if ((_b = this.textContent) == null ? void 0 : _b.plain) {
message.text = this.textContent.plain;
}
message.attachments = this.attachments;
message.headerLines = (this.root.rawHeaderLines || []).slice().reverse();
switch (this.attachmentEncoding) {
case "arraybuffer":
break;
case "base64":
for (let attachment of message.attachments || []) {
if (attachment == null ? void 0 : attachment.content) {
attachment.content = (0, import_base64_encoder.base64ArrayBuffer)(attachment.content);
attachment.encoding = "base64";
}
}
break;
case "utf8":
let attachmentDecoder = new TextDecoder("utf8");
for (let attachment of message.attachments || []) {
if (attachment == null ? void 0 : attachment.content) {
attachment.content = attachmentDecoder.decode(attachment.content);
attachment.encoding = "utf8";
}
}
break;
default:
throw new Error("Unknown attachment encoding");
}
return message;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
addressParser,
decodeWords
});
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

134
node_modules/postal-mime/dist/qp-decoder.cjs generated vendored Normal file
View File

@@ -0,0 +1,134 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var qp_decoder_exports = {};
__export(qp_decoder_exports, {
default: () => QPDecoder
});
module.exports = __toCommonJS(qp_decoder_exports);
var import_decode_strings = require("./decode-strings.cjs");
const VALID_QP_REGEX = /^=[a-f0-9]{2}$/i;
const QP_SPLIT_REGEX = /(?==[a-f0-9]{2})/i;
const SOFT_LINE_BREAK_REGEX = /=\r?\n/g;
const PARTIAL_QP_ENDING_REGEX = /=[a-fA-F0-9]?$/;
class QPDecoder {
constructor(opts) {
opts = opts || {};
this.decoder = opts.decoder || new TextDecoder();
this.maxChunkSize = 100 * 1024;
this.remainder = "";
this.chunks = [];
}
decodeQPBytes(encodedBytes) {
let buf = new ArrayBuffer(encodedBytes.length);
let dataView = new DataView(buf);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, parseInt(encodedBytes[i], 16));
}
return buf;
}
decodeChunks(str) {
str = str.replace(SOFT_LINE_BREAK_REGEX, "");
let list = str.split(QP_SPLIT_REGEX);
let encodedBytes = [];
for (let part of list) {
if (part.charAt(0) !== "=") {
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
}
this.chunks.push(part);
continue;
}
if (part.length === 3) {
if (VALID_QP_REGEX.test(part)) {
encodedBytes.push(part.substr(1));
} else {
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
}
this.chunks.push(part);
}
continue;
}
if (part.length > 3) {
const firstThree = part.substr(0, 3);
if (VALID_QP_REGEX.test(firstThree)) {
encodedBytes.push(part.substr(1, 2));
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
part = part.substr(3);
this.chunks.push(part);
} else {
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
}
this.chunks.push(part);
}
}
}
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
}
}
update(buffer) {
let str = this.decoder.decode(buffer) + "\n";
str = this.remainder + str;
if (str.length < this.maxChunkSize) {
this.remainder = str;
return;
}
this.remainder = "";
let partialEnding = str.match(PARTIAL_QP_ENDING_REGEX);
if (partialEnding) {
if (partialEnding.index === 0) {
this.remainder = str;
return;
}
this.remainder = str.substr(partialEnding.index);
str = str.substr(0, partialEnding.index);
}
this.decodeChunks(str);
}
finalize() {
if (this.remainder.length) {
this.decodeChunks(this.remainder);
this.remainder = "";
}
return (0, import_decode_strings.blobToArrayBuffer)(new Blob(this.chunks, { type: "application/octet-stream" }));
}
}
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

284
node_modules/postal-mime/dist/text-format.cjs generated vendored Normal file
View File

@@ -0,0 +1,284 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var text_format_exports = {};
__export(text_format_exports, {
decodeHTMLEntities: () => decodeHTMLEntities,
escapeHtml: () => escapeHtml,
formatHtmlHeader: () => formatHtmlHeader,
formatTextHeader: () => formatTextHeader,
htmlToText: () => htmlToText,
textToHtml: () => textToHtml
});
module.exports = __toCommonJS(text_format_exports);
var import_html_entities = __toESM(require("./html-entities.cjs"), 1);
function decodeHTMLEntities(str) {
return str.replace(/&(#\d+|#x[a-f0-9]+|[a-z]+\d*);?/gi, (match, entity) => {
if (typeof import_html_entities.default[match] === "string") {
return import_html_entities.default[match];
}
if (entity.charAt(0) !== "#" || match.charAt(match.length - 1) !== ";") {
return match;
}
let codePoint;
if (entity.charAt(1) === "x") {
codePoint = parseInt(entity.substr(2), 16);
} else {
codePoint = parseInt(entity.substr(1), 10);
}
let output = "";
if (codePoint >= 55296 && codePoint <= 57343 || codePoint > 1114111) {
return "\uFFFD";
}
if (codePoint > 65535) {
codePoint -= 65536;
output += String.fromCharCode(codePoint >>> 10 & 1023 | 55296);
codePoint = 56320 | codePoint & 1023;
}
output += String.fromCharCode(codePoint);
return output;
});
}
function escapeHtml(str) {
return str.trim().replace(/[<>"'?&]/g, (c) => {
let hex = c.charCodeAt(0).toString(16);
if (hex.length < 2) {
hex = "0" + hex;
}
return "&#x" + hex.toUpperCase() + ";";
});
}
function textToHtml(str) {
let html = escapeHtml(str).replace(/\n/g, "<br />");
return "<div>" + html + "</div>";
}
function htmlToText(str) {
str = str.replace(/\r?\n/g, "").replace(/<\!\-\-.*?\-\->/gi, " ").replace(/<br\b[^>]*>/gi, "\n").replace(/<\/?(p|div|table|tr|td|th)\b[^>]*>/gi, "\n\n").replace(/<script\b[^>]*>.*?<\/script\b[^>]*>/gi, " ").replace(/^.*<body\b[^>]*>/i, "").replace(/^.*<\/head\b[^>]*>/i, "").replace(/^.*<\!doctype\b[^>]*>/i, "").replace(/<\/body\b[^>]*>.*$/i, "").replace(/<\/html\b[^>]*>.*$/i, "").replace(/<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/gi, " ($1) ").replace(/<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, "").replace(/<li\b[^>]*>[\n\u0001\s]*/gi, "* ").replace(/<hr\b[^>]*>/g, "\n-------------\n").replace(/<[^>]*>/g, " ").replace(/\u0001/g, "\n").replace(/[ \t]+/g, " ").replace(/^\s+$/gm, "").replace(/\n\n+/g, "\n\n").replace(/^\n+/, "\n").replace(/\n+$/, "\n");
str = decodeHTMLEntities(str);
return str;
}
function formatTextAddress(address) {
return [].concat(address.name || []).concat(address.name ? `<${address.address}>` : address.address).join(" ");
}
function formatTextAddresses(addresses) {
let parts = [];
let processAddress = (address, partCounter) => {
if (partCounter) {
parts.push(", ");
}
if (address.group) {
let groupStart = `${address.name}:`;
let groupEnd = `;`;
parts.push(groupStart);
address.group.forEach(processAddress);
parts.push(groupEnd);
} else {
parts.push(formatTextAddress(address));
}
};
addresses.forEach(processAddress);
return parts.join("");
}
function formatHtmlAddress(address) {
return `<a href="mailto:${escapeHtml(address.address)}" class="postal-email-address">${escapeHtml(address.name || `<${address.address}>`)}</a>`;
}
function formatHtmlAddresses(addresses) {
let parts = [];
let processAddress = (address, partCounter) => {
if (partCounter) {
parts.push('<span class="postal-email-address-separator">, </span>');
}
if (address.group) {
let groupStart = `<span class="postal-email-address-group">${escapeHtml(address.name)}:</span>`;
let groupEnd = `<span class="postal-email-address-group">;</span>`;
parts.push(groupStart);
address.group.forEach(processAddress);
parts.push(groupEnd);
} else {
parts.push(formatHtmlAddress(address));
}
};
addresses.forEach(processAddress);
return parts.join(" ");
}
function foldLines(str, lineLength, afterSpace) {
str = (str || "").toString();
lineLength = lineLength || 76;
let pos = 0, len = str.length, result = "", line, match;
while (pos < len) {
line = str.substr(pos, lineLength);
if (line.length < lineLength) {
result += line;
break;
}
if (match = line.match(/^[^\n\r]*(\r?\n|\r)/)) {
line = match[0];
result += line;
pos += line.length;
continue;
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || "").length : 0) < line.length) {
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || "").length : 0)));
} else if (match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/)) {
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || "").length : 0));
}
result += line;
pos += line.length;
if (pos < len) {
result += "\r\n";
}
}
return result;
}
function formatTextHeader(message) {
let rows = [];
if (message.from) {
rows.push({ key: "From", val: formatTextAddress(message.from) });
}
if (message.subject) {
rows.push({ key: "Subject", val: message.subject });
}
if (message.date) {
let dateOptions = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false
};
let dateStr = typeof Intl === "undefined" ? message.date : new Intl.DateTimeFormat("default", dateOptions).format(new Date(message.date));
rows.push({ key: "Date", val: dateStr });
}
if (message.to && message.to.length) {
rows.push({ key: "To", val: formatTextAddresses(message.to) });
}
if (message.cc && message.cc.length) {
rows.push({ key: "Cc", val: formatTextAddresses(message.cc) });
}
if (message.bcc && message.bcc.length) {
rows.push({ key: "Bcc", val: formatTextAddresses(message.bcc) });
}
let maxKeyLength = rows.map((r) => r.key.length).reduce((acc, cur) => {
return cur > acc ? cur : acc;
}, 0);
rows = rows.flatMap((row) => {
let sepLen = maxKeyLength - row.key.length;
let prefix = `${row.key}: ${" ".repeat(sepLen)}`;
let emptyPrefix = `${" ".repeat(row.key.length + 1)} ${" ".repeat(sepLen)}`;
let foldedLines = foldLines(row.val, 80, true).split(/\r?\n/).map((line) => line.trim());
return foldedLines.map((line, i) => `${i ? emptyPrefix : prefix}${line}`);
});
let maxLineLength = rows.map((r) => r.length).reduce((acc, cur) => {
return cur > acc ? cur : acc;
}, 0);
let lineMarker = "-".repeat(maxLineLength);
let template = `
${lineMarker}
${rows.join("\n")}
${lineMarker}
`;
return template;
}
function formatHtmlHeader(message) {
let rows = [];
if (message.from) {
rows.push(
`<div class="postal-email-header-key">From</div><div class="postal-email-header-value">${formatHtmlAddress(message.from)}</div>`
);
}
if (message.subject) {
rows.push(
`<div class="postal-email-header-key">Subject</div><div class="postal-email-header-value postal-email-header-subject">${escapeHtml(
message.subject
)}</div>`
);
}
if (message.date) {
let dateOptions = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false
};
let dateStr = typeof Intl === "undefined" ? message.date : new Intl.DateTimeFormat("default", dateOptions).format(new Date(message.date));
rows.push(
`<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(
message.date
)}">${escapeHtml(dateStr)}</div>`
);
}
if (message.to && message.to.length) {
rows.push(
`<div class="postal-email-header-key">To</div><div class="postal-email-header-value">${formatHtmlAddresses(message.to)}</div>`
);
}
if (message.cc && message.cc.length) {
rows.push(
`<div class="postal-email-header-key">Cc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.cc)}</div>`
);
}
if (message.bcc && message.bcc.length) {
rows.push(
`<div class="postal-email-header-key">Bcc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.bcc)}</div>`
);
}
let template = `<div class="postal-email-header">${rows.length ? '<div class="postal-email-header-row">' : ""}${rows.join(
'</div>\n<div class="postal-email-header-row">'
)}${rows.length ? "</div>" : ""}</div>`;
return template;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
decodeHTMLEntities,
escapeHtml,
formatHtmlHeader,
formatTextHeader,
htmlToText,
textToHtml
});
// Make default export work naturally with require()
if (module.exports.default) {
var defaultExport = module.exports.default;
var namedExports = {};
for (var key in module.exports) {
if (key !== 'default' && key !== '__esModule') {
namedExports[key] = module.exports[key];
}
}
module.exports = defaultExport;
Object.assign(module.exports, namedExports);
// Preserve __esModule and .default for bundler/transpiler interop
Object.defineProperty(module.exports, '__esModule', { value: true });
module.exports.default = defaultExport;
}

51
node_modules/postal-mime/package.json generated vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "postal-mime",
"version": "2.7.4",
"description": "Email parser for Node.js and browser environments",
"main": "./dist/postal-mime.cjs",
"module": "./src/postal-mime.js",
"exports": {
".": {
"types": "./postal-mime.d.ts",
"import": "./src/postal-mime.js",
"require": "./dist/postal-mime.cjs"
}
},
"type": "module",
"types": "postal-mime.d.ts",
"scripts": {
"build": "node scripts/build-cjs.js",
"test": "npm run lint && npm run type-check && node --test",
"lint": "eslint",
"type-check": "tsc --noEmit",
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
"format": "prettier --write \"**/*.{js,mjs,json}\" --ignore-path .prettierignore",
"format:check": "prettier --check \"**/*.{js,mjs,json}\" --ignore-path .prettierignore",
"prepublishOnly": "npm run build"
},
"keywords": [
"mime",
"email"
],
"repository": {
"type": "git",
"url": "git+https://github.com/postalsys/postal-mime.git"
},
"bugs": {
"url": "https://github.com/postalsys/postal-mime/issues"
},
"homepage": "https://postal-mime.postalsys.com",
"author": "Andris Reinman",
"license": "MIT-0",
"devDependencies": {
"@types/node": "25.5.0",
"cross-blob": "3.0.2",
"cross-env": "10.1.0",
"esbuild": "0.27.4",
"eslint": "8.57.0",
"eslint-cli": "1.1.1",
"iframe-resizer": "4.3.6",
"prettier": "3.8.1",
"typescript": "5.9.3"
}
}

94
node_modules/postal-mime/postal-mime.d.ts generated vendored Normal file
View File

@@ -0,0 +1,94 @@
export type RawEmail = string | ArrayBuffer | Uint8Array | Blob | Buffer | ReadableStream;
export type Header = {
/** Lowercase header name */
key: string;
/** Original header name preserving case */
originalKey: string;
/** Header value */
value: string;
};
export type HeaderLine = {
/** Lowercase header name */
key: string;
/** Complete raw header line including key and value (with folded lines merged) */
line: string;
};
export type Mailbox = {
name: string;
address: string;
group?: undefined;
};
export type Address =
| Mailbox
| {
name: string;
address?: undefined;
group: Mailbox[];
};
export type Attachment = {
filename: string | null;
mimeType: string;
disposition: "attachment" | "inline" | null;
related?: boolean;
description?: string;
contentId?: string;
method?: string;
content: ArrayBuffer | Uint8Array | string;
encoding?: "base64" | "utf8";
};
export type Email = {
headers: Header[];
headerLines: HeaderLine[];
from?: Address;
sender?: Address;
replyTo?: Address[];
deliveredTo?: string;
returnPath?: string;
to?: Address[];
cc?: Address[];
bcc?: Address[];
subject?: string;
messageId?: string;
inReplyTo?: string;
references?: string;
date?: string;
html?: string;
text?: string;
attachments: Attachment[];
};
export type AddressParserOptions = {
flatten?: boolean
}
export function addressParser (
str: string,
options?: AddressParserOptions
): Address[];
export function decodeWords (
str: string
): string;
export type PostalMimeOptions = {
rfc822Attachments?: boolean,
forceRfc822Attachments?: boolean,
attachmentEncoding?: "base64" | "utf8" | "arraybuffer",
maxNestingDepth?: number,
maxHeadersSize?: number
}
export default class PostalMime {
constructor(options?: PostalMimeOptions);
static parse(
email: RawEmail,
options?: PostalMimeOptions
): Promise<Email>;
parse(email: RawEmail): Promise<Email>;
}

395
node_modules/postal-mime/src/address-parser.js generated vendored Normal file
View File

@@ -0,0 +1,395 @@
import { decodeWords } from './decode-strings.js';
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @param {Number} depth Current recursion depth for nested group protection
* @return {Object} Address object
*/
function _handleAddress(tokens, depth) {
let isGroup = false;
let state = 'text';
let address;
let addresses = [];
let data = {
address: [],
comment: [],
group: [],
text: [],
textWasQuoted: [] // Track which text tokens came from inside quotes
};
let i;
let len;
let insideQuotes = false; // Track if we're currently inside a quoted string
// Filter out <addresses>, (comments) and regular text
for (i = 0, len = tokens.length; i < len; i++) {
let token = tokens[i];
let prevToken = i ? tokens[i - 1] : null;
if (token.type === 'operator') {
switch (token.value) {
case '<':
state = 'address';
insideQuotes = false;
break;
case '(':
state = 'comment';
insideQuotes = false;
break;
case ':':
state = 'group';
isGroup = true;
insideQuotes = false;
break;
case '"':
// Track quote state for text tokens
insideQuotes = !insideQuotes;
state = 'text';
break;
default:
state = 'text';
insideQuotes = false;
break;
}
} else if (token.value) {
if (state === 'address') {
// handle use case where unquoted name includes a "<"
// Apple Mail truncates everything between an unexpected < and an address
// and so will we
token.value = token.value.replace(/^[^<]*<\s*/, '');
}
if (prevToken && prevToken.noBreak && data[state].length) {
// join values
data[state][data[state].length - 1] += token.value;
if (state === 'text' && insideQuotes) {
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
}
} else {
data[state].push(token.value);
if (state === 'text') {
data.textWasQuoted.push(insideQuotes);
}
}
}
}
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
let groupMembers = [];
if (data.group.length) {
let parsedGroup = addressParser(data.group.join(','), { _depth: depth + 1 });
// Flatten: if any member is itself a group, extract its members into the sequence
parsedGroup.forEach(member => {
if (member.group) {
// Nested group detected - flatten it by adding its members directly
groupMembers = groupMembers.concat(member.group);
} else {
groupMembers.push(member);
}
});
}
addresses.push({
name: decodeWords(data.text || (address && address.name)),
group: groupMembers
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
for (i = data.text.length - 1; i >= 0; i--) {
// Security fix: Do not extract email addresses from quoted strings
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
// Extracting emails from quoted text leads to misrouting vulnerabilities
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
data.textWasQuoted.splice(i, 1);
break;
}
}
let _regexHandler = function (address) {
if (!data.address.length) {
data.address = [address.trim()];
return ' ';
} else {
return address;
}
};
// still no address
if (!data.address.length) {
for (i = data.text.length - 1; i >= 0; i--) {
// Security fix: Do not extract email addresses from quoted strings
if (!data.textWasQuoted[i]) {
// fixed the regex to parse email address correctly when email address has more than one @
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
if (data.address.length) {
break;
}
}
}
}
}
// If there's still no text but a comment exists, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurrence, push others to regular text
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
// Join values with spaces
data.text = data.text.join(' ');
data.address = data.address.join(' ');
if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
// try to extract words from text content
const decodedText = decodeWords(data.text);
// Security: only re-parse if decoded text contains angle-bracket addresses.
// Without this, a bare encoded email (e.g. =?utf-8?B?dGVzdEBldmlsLmNv?=)
// would be fabricated into an address from attacker-controlled input.
if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
const parsedSubAddresses = addressParser(decodedText);
if (parsedSubAddresses && parsedSubAddresses.length) {
return parsedSubAddresses;
}
}
// No usable address found - treat decoded text as display name only
return [{ address: '', name: decodedText }];
}
address = {
address: data.address || data.text || '',
name: decodeWords(data.text || data.address || '')
};
if (address.address === address.name) {
if ((address.address || '').match(/@/)) {
address.name = '';
} else {
address.address = '';
}
}
addresses.push(address);
}
return addresses;
}
/**
* Creates a Tokenizer object for tokenizing address field strings
*
* @constructor
* @param {String} str Address field string
*/
class Tokenizer {
constructor(str) {
this.str = (str || '').toString();
this.operatorCurrent = '';
this.operatorExpecting = '';
this.node = null;
this.escaped = false;
this.list = [];
/**
* Operator tokens and which tokens are expected to end the sequence
*/
this.operators = {
'"': '"',
'(': ')',
'<': '>',
',': '',
':': ';',
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
';': ''
};
}
/**
* Tokenizes the original input string
*
* @return {Array} An array of operator|text tokens
*/
tokenize() {
let list = [];
for (let i = 0, len = this.str.length; i < len; i++) {
let chr = this.str.charAt(i);
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
this.checkChar(chr, nextChr);
}
this.list.forEach(node => {
node.value = (node.value || '').toString().trim();
if (node.value) {
list.push(node);
}
});
return list;
}
/**
* Checks if a character is an operator or text and acts accordingly
*
* @param {String} chr Character from the address field
*/
checkChar(chr, nextChr) {
if (this.escaped) {
// ignore next condition blocks
} else if (chr === this.operatorExpecting) {
this.node = {
type: 'operator',
value: chr
};
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
this.node.noBreak = true;
}
this.list.push(this.node);
this.node = null;
this.operatorExpecting = '';
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in this.operators) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = this.operators[chr];
this.escaped = false;
return;
} else if (this.operatorExpecting === '"' && chr === '\\') {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: 'text',
value: ''
};
this.list.push(this.node);
}
if (chr === '\n') {
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
chr = ' ';
}
if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
// skip command bytes
this.node.value += chr;
}
this.escaped = false;
}
}
/**
* Maximum recursion depth for parsing nested groups.
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
* malicious input that could cause stack overflow.
*/
const MAX_NESTED_GROUP_DEPTH = 50;
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* 'Name <address@domain>'
*
* will be converted to
*
* [{name: 'Name', address: 'address@domain'}]
*
* @param {String} str Address field
* @param {Object} options Optional options object
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
* @return {Array} An array of address objects
*/
function addressParser(str, options) {
options = options || {};
let depth = options._depth || 0;
// Prevent stack overflow from deeply nested groups (DoS protection)
if (depth > MAX_NESTED_GROUP_DEPTH) {
return [];
}
let tokenizer = new Tokenizer(str);
let tokens = tokenizer.tokenize();
let addresses = [];
let address = [];
let parsedAddresses = [];
tokens.forEach(token => {
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
if (address.length) {
addresses.push(address);
}
address = [];
} else {
address.push(token);
}
});
if (address.length) {
addresses.push(address);
}
addresses.forEach(address => {
address = _handleAddress(address, depth);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}
});
if (options.flatten) {
let addresses = [];
let walkAddressList = list => {
list.forEach(address => {
if (address.group) {
return walkAddressList(address.group);
} else {
addresses.push(address);
}
});
};
walkAddressList(parsedAddresses);
return addresses;
}
return parsedAddresses;
}
// expose to the world
export default addressParser;

48
node_modules/postal-mime/src/base64-decoder.js generated vendored Normal file
View File

@@ -0,0 +1,48 @@
import { decodeBase64, blobToArrayBuffer } from './decode-strings.js';
export default class Base64Decoder {
constructor(opts) {
opts = opts || {};
this.decoder = opts.decoder || new TextDecoder();
this.maxChunkSize = 100 * 1024;
this.chunks = [];
this.remainder = '';
}
update(buffer) {
let str = this.decoder.decode(buffer);
str = str.replace(/[^a-zA-Z0-9+\/]+/g, '');
this.remainder += str;
if (this.remainder.length >= this.maxChunkSize) {
let allowedBytes = Math.floor(this.remainder.length / 4) * 4;
let base64Str;
if (allowedBytes === this.remainder.length) {
base64Str = this.remainder;
this.remainder = '';
} else {
base64Str = this.remainder.substr(0, allowedBytes);
this.remainder = this.remainder.substr(allowedBytes);
}
if (base64Str.length) {
this.chunks.push(decodeBase64(base64Str));
}
}
}
finalize() {
if (this.remainder && !/^=+$/.test(this.remainder)) {
this.chunks.push(decodeBase64(this.remainder));
}
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
}
}

69
node_modules/postal-mime/src/base64-encoder.js generated vendored Normal file
View File

@@ -0,0 +1,69 @@
// Code from: https://gist.githubusercontent.com/jonleighton/958841/raw/fb05a8632efb75d85d43deb593df04367ce48371/base64ArrayBuffer.js
// Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then
// use window.btoa' step. According to my tests, this appears to be a faster approach:
// http://jsperf.com/encoding-xhr-image-data/5
/*
MIT LICENSE
Copyright 2011 Jon Leighton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export function base64ArrayBuffer(arrayBuffer) {
var base64 = '';
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var bytes = new Uint8Array(arrayBuffer);
var byteLength = bytes.byteLength;
var byteRemainder = byteLength % 3;
var mainLength = byteLength - byteRemainder;
var a, b, c, d;
var chunk;
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '==';
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '=';
}
return base64;
}

287
node_modules/postal-mime/src/decode-strings.js generated vendored Normal file
View File

@@ -0,0 +1,287 @@
export const textEncoder = new TextEncoder();
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
const base64Lookup = new Uint8Array(256);
for (let i = 0; i < base64Chars.length; i++) {
base64Lookup[base64Chars.charCodeAt(i)] = i;
}
export function decodeBase64(base64) {
let bufferLength = Math.ceil(base64.length / 4) * 3;
const len = base64.length;
let p = 0;
if (base64.length % 4 === 3) {
bufferLength--;
} else if (base64.length % 4 === 2) {
bufferLength -= 2;
} else if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}
const arrayBuffer = new ArrayBuffer(bufferLength);
const bytes = new Uint8Array(arrayBuffer);
for (let i = 0; i < len; i += 4) {
let encoded1 = base64Lookup[base64.charCodeAt(i)];
let encoded2 = base64Lookup[base64.charCodeAt(i + 1)];
let encoded3 = base64Lookup[base64.charCodeAt(i + 2)];
let encoded4 = base64Lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arrayBuffer;
}
export function getDecoder(charset) {
charset = charset || 'utf8';
let decoder;
try {
decoder = new TextDecoder(charset);
} catch (err) {
decoder = new TextDecoder('windows-1252');
}
return decoder;
}
/**
* Converts a Blob into an ArrayBuffer
* @param {Blob} blob Blob to convert
* @returns {ArrayBuffer} Converted value
*/
export async function blobToArrayBuffer(blob) {
if ('arrayBuffer' in blob) {
return await blob.arrayBuffer();
}
const fr = new FileReader();
return new Promise((resolve, reject) => {
fr.onload = function (e) {
resolve(e.target.result);
};
fr.onerror = function (e) {
reject(fr.error);
};
fr.readAsArrayBuffer(blob);
});
}
export function getHex(c) {
if (
(c >= 0x30 /* 0 */ && c <= 0x39) /* 9 */ ||
(c >= 0x61 /* a */ && c <= 0x66) /* f */ ||
(c >= 0x41 /* A */ && c <= 0x46) /* F */
) {
return String.fromCharCode(c);
}
return false;
}
/**
* Decode a complete mime word encoded string
*
* @param {String} str Mime word encoded string
* @return {String} Decoded unicode string
*/
export function decodeWord(charset, encoding, str) {
// RFC2231 added language tag to the encoding
// see: https://tools.ietf.org/html/rfc2231#section-5
// this implementation silently ignores this tag
let splitPos = charset.indexOf('*');
if (splitPos >= 0) {
charset = charset.substr(0, splitPos);
}
encoding = encoding.toUpperCase();
let byteStr;
if (encoding === 'Q') {
str = str
// remove spaces between = and hex char, this might indicate invalidly applied line splitting
.replace(/=\s+([0-9a-fA-F])/g, '=$1')
// convert all underscores to spaces
.replace(/[_\s]/g, ' ');
let buf = textEncoder.encode(str);
let encodedBytes = [];
for (let i = 0, len = buf.length; i < len; i++) {
let c = buf[i];
if (i <= len - 2 && c === 0x3d /* = */) {
let c1 = getHex(buf[i + 1]);
let c2 = getHex(buf[i + 2]);
if (c1 && c2) {
let c = parseInt(c1 + c2, 16);
encodedBytes.push(c);
i += 2;
continue;
}
}
encodedBytes.push(c);
}
byteStr = new ArrayBuffer(encodedBytes.length);
let dataView = new DataView(byteStr);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, encodedBytes[i]);
}
} else if (encoding === 'B') {
byteStr = decodeBase64(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ''));
} else {
// keep as is, convert ArrayBuffer to unicode string, assume utf8
byteStr = textEncoder.encode(str);
}
return getDecoder(charset).decode(byteStr);
}
export function decodeWords(str) {
let joinString = true;
let done = false;
while (!done) {
let result = (str || '')
.toString()
// find base64 words that can be joined
.replace(
/(=\?([^?]+)\?[Bb]\?([^?]*)\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g,
(match, left, chLeft, encodedLeftStr, chRight) => {
if (!joinString) {
return match;
}
// only mark b64 chunks to be joined if charsets match and left side does not end with =
if (chLeft === chRight && encodedLeftStr.length % 4 === 0 && !/=$/.test(encodedLeftStr)) {
// set a joiner marker
return left + '__\x00JOIN\x00__';
}
return match;
}
)
// find QP words that can be joined
.replace(
/(=\?([^?]+)\?[Qq]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Qq]\?[^?]*\?=)/g,
(match, left, chLeft, chRight) => {
if (!joinString) {
return match;
}
// only mark QP chunks to be joined if charsets match
if (chLeft === chRight) {
// set a joiner marker
return left + '__\x00JOIN\x00__';
}
return match;
}
)
// join base64 encoded words
.replace(/(\?=)?__\x00JOIN\x00__(=\?([^?]+)\?[QqBb]\?)?/g, '')
// remove spaces between mime encoded words
.replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, '$1')
// decode words
.replace(/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g, (m, charset, encoding, text) =>
decodeWord(charset, encoding, text)
);
if (joinString && result.indexOf('\ufffd') >= 0) {
// text contains \ufffd (EF BF BD), so unicode conversion failed, retry without joining strings
joinString = false;
} else {
return result;
}
}
}
export function decodeURIComponentWithCharset(encodedStr, charset) {
charset = charset || 'utf-8';
let encodedBytes = [];
for (let i = 0; i < encodedStr.length; i++) {
let c = encodedStr.charAt(i);
if (c === '%' && /^[a-f0-9]{2}/i.test(encodedStr.substr(i + 1, 2))) {
// encoded sequence
let byte = encodedStr.substr(i + 1, 2);
i += 2;
encodedBytes.push(parseInt(byte, 16));
} else if (c.charCodeAt(0) > 126) {
c = textEncoder.encode(c);
for (let j = 0; j < c.length; j++) {
encodedBytes.push(c[j]);
}
} else {
// "normal" char
encodedBytes.push(c.charCodeAt(0));
}
}
const byteStr = new ArrayBuffer(encodedBytes.length);
const dataView = new DataView(byteStr);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, encodedBytes[i]);
}
return getDecoder(charset).decode(byteStr);
}
export function decodeParameterValueContinuations(header) {
// handle parameter value continuations
// https://tools.ietf.org/html/rfc2231#section-3
// preprocess values
let paramKeys = new Map();
Object.keys(header.params).forEach(key => {
let match = key.match(/\*((\d+)\*?)?$/);
if (!match) {
// nothing to do here, does not seem like a continuation param
return;
}
let actualKey = key.substr(0, match.index).toLowerCase();
let nr = Number(match[2]) || 0;
let paramVal;
if (!paramKeys.has(actualKey)) {
paramVal = {
charset: false,
values: []
};
paramKeys.set(actualKey, paramVal);
} else {
paramVal = paramKeys.get(actualKey);
}
let value = header.params[key];
if (nr === 0 && match[0].charAt(match[0].length - 1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
paramVal.charset = match[1] || 'utf-8';
value = match[2];
}
paramVal.values.push({ nr, value });
// remove the old reference
delete header.params[key];
});
paramKeys.forEach((paramVal, key) => {
header.params[key] = decodeURIComponentWithCharset(
paramVal.values
.sort((a, b) => a.nr - b.nr)
.map(a => a.value)
.join(''),
paramVal.charset
);
});
}

2236
node_modules/postal-mime/src/html-entities.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

373
node_modules/postal-mime/src/mime-node.js generated vendored Normal file
View File

@@ -0,0 +1,373 @@
import { getDecoder, decodeParameterValueContinuations, textEncoder } from './decode-strings.js';
import PassThroughDecoder from './pass-through-decoder.js';
import Base64Decoder from './base64-decoder.js';
import QPDecoder from './qp-decoder.js';
const defaultDecoder = getDecoder();
export default class MimeNode {
constructor(options) {
this.options = options || {};
this.postalMime = this.options.postalMime;
this.root = !!this.options.parentNode;
this.childNodes = [];
if (this.options.parentNode) {
this.parentNode = this.options.parentNode;
this.depth = this.parentNode.depth + 1;
if (this.depth > this.options.maxNestingDepth) {
throw new Error(`Maximum MIME nesting depth of ${this.options.maxNestingDepth} levels exceeded`);
}
this.options.parentNode.childNodes.push(this);
} else {
this.depth = 0;
}
this.state = 'header';
this.headerLines = [];
this.headerSize = 0;
// RFC 2046 Section 5.1.5: multipart/digest defaults to message/rfc822
const parentMultipartType = this.options.parentMultipartType || null;
const defaultContentType = parentMultipartType === 'digest' ? 'message/rfc822' : 'text/plain';
this.contentType = {
value: defaultContentType,
default: true
};
this.contentTransferEncoding = {
value: '8bit'
};
this.contentDisposition = {
value: ''
};
this.headers = [];
this.contentDecoder = false;
}
setupContentDecoder(transferEncoding) {
if (/base64/i.test(transferEncoding)) {
this.contentDecoder = new Base64Decoder();
} else if (/quoted-printable/i.test(transferEncoding)) {
this.contentDecoder = new QPDecoder({ decoder: getDecoder(this.contentType.parsed.params.charset) });
} else {
this.contentDecoder = new PassThroughDecoder();
}
}
async finalize() {
if (this.state === 'finished') {
return;
}
if (this.state === 'header') {
this.processHeaders();
}
// remove self from boundary listing
let boundaries = this.postalMime.boundaries;
for (let i = boundaries.length - 1; i >= 0; i--) {
let boundary = boundaries[i];
if (boundary.node === this) {
boundaries.splice(i, 1);
break;
}
}
await this.finalizeChildNodes();
this.content = this.contentDecoder ? await this.contentDecoder.finalize() : null;
this.state = 'finished';
}
async finalizeChildNodes() {
for (let childNode of this.childNodes) {
await childNode.finalize();
}
}
// Strip RFC 822 comments (parenthesized text) from structured header values
stripComments(str) {
let result = '';
let depth = 0;
let escaped = false;
let inQuote = false;
for (let i = 0; i < str.length; i++) {
const chr = str.charAt(i);
if (escaped) {
if (depth === 0) {
result += chr;
}
escaped = false;
continue;
}
if (chr === '\\') {
escaped = true;
if (depth === 0) {
result += chr;
}
continue;
}
if (chr === '"' && depth === 0) {
inQuote = !inQuote;
result += chr;
continue;
}
if (!inQuote) {
if (chr === '(') {
depth++;
continue;
}
if (chr === ')' && depth > 0) {
depth--;
continue;
}
}
if (depth === 0) {
result += chr;
}
}
return result;
}
parseStructuredHeader(str) {
// Strip RFC 822 comments before parsing
str = this.stripComments(str);
let response = {
value: false,
params: {}
};
let key = false;
let value = '';
let stage = 'value';
let quote = false;
let escaped = false;
let chr;
for (let i = 0, len = str.length; i < len; i++) {
chr = str.charAt(i);
switch (stage) {
case 'key':
if (chr === '=') {
key = value.trim().toLowerCase();
stage = 'value';
value = '';
break;
}
value += chr;
break;
case 'value':
if (escaped) {
value += chr;
} else if (chr === '\\') {
escaped = true;
continue;
} else if (quote && chr === quote) {
quote = false;
} else if (!quote && chr === '"') {
quote = chr;
} else if (!quote && chr === ';') {
if (key === false) {
response.value = value.trim();
} else {
response.params[key] = value.trim();
}
stage = 'key';
value = '';
} else {
value += chr;
}
escaped = false;
break;
}
}
// finalize remainder
value = value.trim();
if (stage === 'value') {
if (key === false) {
// default value
response.value = value;
} else {
// subkey value
response.params[key] = value;
}
} else if (value) {
// treat as key without value, see emptykey:
// Header-Key: somevalue; key=value; emptykey
response.params[value.toLowerCase()] = '';
}
if (response.value) {
response.value = response.value.toLowerCase();
}
// convert Parameter Value Continuations into single strings
decodeParameterValueContinuations(response);
return response;
}
decodeFlowedText(str, delSp) {
return (
str
.split(/\r?\n/)
// remove soft linebreaks
// soft linebreaks are added after space symbols
.reduce((previousValue, currentValue) => {
if (previousValue.endsWith(' ') && previousValue !== '-- ' && !previousValue.endsWith('\n-- ')) {
if (delSp) {
// delsp adds space to text to be able to fold it
// these spaces can be removed once the text is unfolded
return previousValue.slice(0, -1) + currentValue;
} else {
return previousValue + currentValue;
}
} else {
return previousValue + '\n' + currentValue;
}
})
// remove whitespace stuffing
// http://tools.ietf.org/html/rfc3676#section-4.4
.replace(/^ /gm, '')
);
}
getTextContent() {
if (!this.content) {
return '';
}
let str = getDecoder(this.contentType.parsed.params.charset).decode(this.content);
if (/^flowed$/i.test(this.contentType.parsed.params.format)) {
str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp));
}
return str;
}
processHeaders() {
// First pass: merge folded headers (backward iteration)
for (let i = this.headerLines.length - 1; i >= 0; i--) {
let line = this.headerLines[i];
if (i && /^\s/.test(line)) {
this.headerLines[i - 1] += '\n' + line;
this.headerLines.splice(i, 1);
}
}
// Initialize rawHeaderLines to store unmodified lines
this.rawHeaderLines = [];
// Second pass: process headers (MUST be backward to maintain this.headers order)
// The existing code iterates backward and postal-mime.js calls .reverse()
// We must preserve this behavior to avoid breaking changes
for (let i = this.headerLines.length - 1; i >= 0; i--) {
let rawLine = this.headerLines[i];
// Extract key from raw line for rawHeaderLines
let sep = rawLine.indexOf(':');
let rawKey = sep < 0 ? rawLine.trim() : rawLine.substr(0, sep).trim();
// Store raw line with lowercase key
this.rawHeaderLines.push({
key: rawKey.toLowerCase(),
line: rawLine
});
// Normalize for this.headers (existing behavior - order preserved)
let normalizedLine = rawLine.replace(/\s+/g, ' ');
sep = normalizedLine.indexOf(':');
let key = sep < 0 ? normalizedLine.trim() : normalizedLine.substr(0, sep).trim();
let value = sep < 0 ? '' : normalizedLine.substr(sep + 1).trim();
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
switch (key.toLowerCase()) {
case 'content-type':
if (this.contentType.default) {
this.contentType = { value, parsed: {} };
}
break;
case 'content-transfer-encoding':
this.contentTransferEncoding = { value, parsed: {} };
break;
case 'content-disposition':
this.contentDisposition = { value, parsed: {} };
break;
case 'content-id':
this.contentId = value;
break;
case 'content-description':
this.contentDescription = value;
break;
}
}
this.contentType.parsed = this.parseStructuredHeader(this.contentType.value);
this.contentType.multipart = /^multipart\//i.test(this.contentType.parsed.value)
? this.contentType.parsed.value.substr(this.contentType.parsed.value.indexOf('/') + 1)
: false;
if (this.contentType.multipart && this.contentType.parsed.params.boundary) {
// add self to boundary terminator listing
this.postalMime.boundaries.push({
value: textEncoder.encode(this.contentType.parsed.params.boundary),
node: this
});
}
this.contentDisposition.parsed = this.parseStructuredHeader(this.contentDisposition.value);
this.contentTransferEncoding.encoding = this.contentTransferEncoding.value
.toLowerCase()
.split(/[^\w-]/)
.shift();
this.setupContentDecoder(this.contentTransferEncoding.encoding);
}
feed(line) {
switch (this.state) {
case 'header':
if (!line.length) {
this.state = 'body';
return this.processHeaders();
}
this.headerSize += line.length;
if (this.headerSize > this.options.maxHeadersSize) {
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
throw error;
}
this.headerLines.push(defaultDecoder.decode(line));
break;
case 'body': {
// add line to body
this.contentDecoder.update(line);
}
}
}
}

3
node_modules/postal-mime/src/package.json generated vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

17
node_modules/postal-mime/src/pass-through-decoder.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
import { blobToArrayBuffer } from './decode-strings.js';
export default class PassThroughDecoder {
constructor() {
this.chunks = [];
}
update(line) {
this.chunks.push(line);
this.chunks.push('\n');
}
finalize() {
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
}
}

584
node_modules/postal-mime/src/postal-mime.js generated vendored Normal file
View File

@@ -0,0 +1,584 @@
import MimeNode from './mime-node.js';
import { textToHtml, htmlToText, formatTextHeader, formatHtmlHeader } from './text-format.js';
import addressParser from './address-parser.js';
import { decodeWords, textEncoder, blobToArrayBuffer } from './decode-strings.js';
import { base64ArrayBuffer } from './base64-encoder.js';
export { addressParser, decodeWords };
const MAX_NESTING_DEPTH = 256;
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
function toCamelCase(key) {
return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
}
export default class PostalMime {
static parse(buf, options) {
const parser = new PostalMime(options);
return parser.parse(buf);
}
constructor(options) {
this.options = options || {};
this.mimeOptions = {
maxNestingDepth: this.options.maxNestingDepth || MAX_NESTING_DEPTH,
maxHeadersSize: this.options.maxHeadersSize || MAX_HEADERS_SIZE
};
this.root = this.currentNode = new MimeNode({
postalMime: this,
...this.mimeOptions
});
this.boundaries = [];
this.textContent = {};
this.attachments = [];
this.attachmentEncoding =
(this.options.attachmentEncoding || '')
.toString()
.replace(/[-_\s]/g, '')
.trim()
.toLowerCase() || 'arraybuffer';
this.started = false;
}
async finalize() {
// close all pending nodes
await this.root.finalize();
}
async processLine(line, isFinal) {
let boundaries = this.boundaries;
// check if this is a mime boundary
if (boundaries.length && line.length > 2 && line[0] === 0x2d && line[1] === 0x2d) {
// could be a boundary marker
for (let i = boundaries.length - 1; i >= 0; i--) {
let boundary = boundaries[i];
// Line must be at least long enough for "--" + boundary
if (line.length < boundary.value.length + 2) {
continue;
}
// Check if boundary value matches
let boundaryMatches = true;
for (let j = 0; j < boundary.value.length; j++) {
if (line[j + 2] !== boundary.value[j]) {
boundaryMatches = false;
break;
}
}
if (!boundaryMatches) {
continue;
}
// Check for terminator (-- after boundary) and determine where boundary ends
let boundaryEnd = boundary.value.length + 2;
let isTerminator = false;
if (
line.length >= boundary.value.length + 4 &&
line[boundary.value.length + 2] === 0x2d &&
line[boundary.value.length + 3] === 0x2d
) {
isTerminator = true;
boundaryEnd = boundary.value.length + 4;
}
// RFC 2046: boundary line may have trailing whitespace (space/tab) before CRLF
let hasValidTrailing = true;
for (let j = boundaryEnd; j < line.length; j++) {
if (line[j] !== 0x20 && line[j] !== 0x09) {
hasValidTrailing = false;
break;
}
}
if (!hasValidTrailing) {
continue;
}
if (isTerminator) {
await boundary.node.finalize();
this.currentNode = boundary.node.parentNode || this.root;
} else {
// finalize any open child nodes (should be just one though)
await boundary.node.finalizeChildNodes();
this.currentNode = new MimeNode({
postalMime: this,
parentNode: boundary.node,
parentMultipartType: boundary.node.contentType.multipart,
...this.mimeOptions
});
}
if (isFinal) {
return this.finalize();
}
return;
}
}
this.currentNode.feed(line);
if (isFinal) {
return this.finalize();
}
}
readLine() {
let startPos = this.readPos;
let endPos = this.readPos;
while (this.readPos < this.av.length) {
const c = this.av[this.readPos++];
if (c !== 0x0d && c !== 0x0a) {
endPos = this.readPos;
}
if (c === 0x0a) {
return {
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
done: this.readPos >= this.av.length
};
}
}
return {
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
done: this.readPos >= this.av.length
};
}
async processNodeTree() {
// get text nodes
let textContent = {};
let textTypes = new Set();
let textMap = (this.textMap = new Map());
let forceRfc822Attachments = this.forceRfc822Attachments();
let walk = async (node, alternative, related) => {
alternative = alternative || false;
related = related || false;
if (!node.contentType.multipart) {
// is it inline message/rfc822
if (this.isInlineMessageRfc822(node) && !forceRfc822Attachments) {
const subParser = new PostalMime();
node.subMessage = await subParser.parse(node.content);
if (!textMap.has(node)) {
textMap.set(node, {});
}
let textEntry = textMap.get(node);
// default to text if there is no content
if (node.subMessage.text || !node.subMessage.html) {
textEntry.plain = textEntry.plain || [];
textEntry.plain.push({ type: 'subMessage', value: node.subMessage });
textTypes.add('plain');
}
if (node.subMessage.html) {
textEntry.html = textEntry.html || [];
textEntry.html.push({ type: 'subMessage', value: node.subMessage });
textTypes.add('html');
}
if (subParser.textMap) {
subParser.textMap.forEach((subTextEntry, subTextNode) => {
textMap.set(subTextNode, subTextEntry);
});
}
for (let attachment of node.subMessage.attachments || []) {
this.attachments.push(attachment);
}
}
// is it text?
else if (this.isInlineTextNode(node)) {
let textType = node.contentType.parsed.value.substr(node.contentType.parsed.value.indexOf('/') + 1);
let selectorNode = alternative || node;
if (!textMap.has(selectorNode)) {
textMap.set(selectorNode, {});
}
let textEntry = textMap.get(selectorNode);
textEntry[textType] = textEntry[textType] || [];
textEntry[textType].push({ type: 'text', value: node.getTextContent() });
textTypes.add(textType);
}
// is it an attachment
else if (node.content) {
const filename =
node.contentDisposition?.parsed?.params?.filename ||
node.contentType.parsed.params.name ||
null;
const attachment = {
filename: filename ? decodeWords(filename) : null,
mimeType: node.contentType.parsed.value,
disposition: node.contentDisposition?.parsed?.value || null
};
if (related && node.contentId) {
attachment.related = true;
}
if (node.contentDescription) {
attachment.description = node.contentDescription;
}
if (node.contentId) {
attachment.contentId = node.contentId;
}
switch (node.contentType.parsed.value) {
// Special handling for calendar events
case 'text/calendar':
case 'application/ics': {
if (node.contentType.parsed.params.method) {
attachment.method = node.contentType.parsed.params.method
.toString()
.toUpperCase()
.trim();
}
// Enforce into unicode
const decodedText = node.getTextContent().replace(/\r?\n/g, '\n').replace(/\n*$/, '\n');
attachment.content = textEncoder.encode(decodedText);
break;
}
// Regular attachments
default:
attachment.content = node.content;
}
this.attachments.push(attachment);
}
} else if (node.contentType.multipart === 'alternative') {
alternative = node;
} else if (node.contentType.multipart === 'related') {
related = node;
}
for (let childNode of node.childNodes) {
await walk(childNode, alternative, related);
}
};
await walk(this.root, false, false);
textMap.forEach(mapEntry => {
textTypes.forEach(textType => {
if (!textContent[textType]) {
textContent[textType] = [];
}
if (mapEntry[textType]) {
mapEntry[textType].forEach(textEntry => {
switch (textEntry.type) {
case 'text':
textContent[textType].push(textEntry.value);
break;
case 'subMessage':
{
switch (textType) {
case 'html':
textContent[textType].push(formatHtmlHeader(textEntry.value));
break;
case 'plain':
textContent[textType].push(formatTextHeader(textEntry.value));
break;
}
}
break;
}
});
} else {
let alternativeType;
switch (textType) {
case 'html':
alternativeType = 'plain';
break;
case 'plain':
alternativeType = 'html';
break;
}
(mapEntry[alternativeType] || []).forEach(textEntry => {
switch (textEntry.type) {
case 'text':
switch (textType) {
case 'html':
textContent[textType].push(textToHtml(textEntry.value));
break;
case 'plain':
textContent[textType].push(htmlToText(textEntry.value));
break;
}
break;
case 'subMessage':
{
switch (textType) {
case 'html':
textContent[textType].push(formatHtmlHeader(textEntry.value));
break;
case 'plain':
textContent[textType].push(formatTextHeader(textEntry.value));
break;
}
}
break;
}
});
}
});
});
Object.keys(textContent).forEach(textType => {
textContent[textType] = textContent[textType].join('\n');
});
this.textContent = textContent;
}
isInlineTextNode(node) {
if (node.contentDisposition?.parsed?.value === 'attachment') {
// no matter the type, this is an attachment
return false;
}
switch (node.contentType.parsed?.value) {
case 'text/html':
case 'text/plain':
return true;
case 'text/calendar':
case 'text/csv':
default:
return false;
}
}
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';
}
// Check if this is a specially crafted report email where message/rfc822 content should not be inlined
forceRfc822Attachments() {
if (this.options.forceRfc822Attachments) {
return true;
}
let forceRfc822Attachments = false;
let walk = node => {
if (!node.contentType.multipart) {
if (
node.contentType.parsed &&
['message/delivery-status', 'message/feedback-report'].includes(node.contentType.parsed.value)
) {
forceRfc822Attachments = true;
}
}
for (let childNode of node.childNodes) {
walk(childNode);
}
};
walk(this.root);
return forceRfc822Attachments;
}
async resolveStream(stream) {
let chunkLen = 0;
let chunks = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
chunkLen += value.length;
}
const result = new Uint8Array(chunkLen);
let chunkPointer = 0;
for (let chunk of chunks) {
result.set(chunk, chunkPointer);
chunkPointer += chunk.length;
}
return result;
}
async parse(buf) {
if (this.started) {
throw new Error('Can not reuse parser, create a new PostalMime object');
}
this.started = true;
// Check if the input is a readable stream and resolve it into an ArrayBuffer
if (buf && typeof buf.getReader === 'function') {
buf = await this.resolveStream(buf);
}
// Should it throw for an empty value instead of defaulting to an empty ArrayBuffer?
buf = buf || new ArrayBuffer(0);
// Cast string input to Uint8Array
if (typeof buf === 'string') {
buf = textEncoder.encode(buf);
}
// Cast Blob to ArrayBuffer
if (buf instanceof Blob || Object.prototype.toString.call(buf) === '[object Blob]') {
buf = await blobToArrayBuffer(buf);
}
// Cast Node.js Buffer object or Uint8Array into ArrayBuffer
if (buf.buffer instanceof ArrayBuffer) {
buf = new Uint8Array(buf).buffer;
}
this.buf = buf;
this.av = new Uint8Array(buf);
this.readPos = 0;
while (this.readPos < this.av.length) {
const line = this.readLine();
await this.processLine(line.bytes, line.done);
}
await this.processNodeTree();
const message = {
headers: this.root.headers
.map(entry => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value }))
.reverse()
};
for (const key of ['from', 'sender']) {
const addressHeader = this.root.headers.find(line => line.key === key);
if (addressHeader && addressHeader.value) {
const addresses = addressParser(addressHeader.value);
if (addresses && addresses.length) {
message[key] = addresses[0];
}
}
}
for (const key of ['delivered-to', 'return-path']) {
const addressHeader = this.root.headers.find(line => line.key === key);
if (addressHeader && addressHeader.value) {
const addresses = addressParser(addressHeader.value);
if (addresses && addresses.length && addresses[0].address) {
const camelKey = toCamelCase(key);
message[camelKey] = addresses[0].address;
}
}
}
for (const key of ['to', 'cc', 'bcc', 'reply-to']) {
const addressHeaders = this.root.headers.filter(line => line.key === key);
let addresses = [];
addressHeaders
.filter(entry => entry && entry.value)
.map(entry => addressParser(entry.value))
.forEach(parsed => (addresses = addresses.concat(parsed || [])));
if (addresses && addresses.length) {
const camelKey = toCamelCase(key);
message[camelKey] = addresses;
}
}
for (const key of ['subject', 'message-id', 'in-reply-to', 'references']) {
const header = this.root.headers.find(line => line.key === key);
if (header && header.value) {
const camelKey = toCamelCase(key);
message[camelKey] = decodeWords(header.value);
}
}
let dateHeader = this.root.headers.find(line => line.key === 'date');
if (dateHeader) {
let date = new Date(dateHeader.value);
if (date.toString() === 'Invalid Date') {
date = dateHeader.value;
} else {
// enforce ISO format if seems to be a valid date
date = date.toISOString();
}
message.date = date;
}
if (this.textContent?.html) {
message.html = this.textContent.html;
}
if (this.textContent?.plain) {
message.text = this.textContent.plain;
}
message.attachments = this.attachments;
// Expose raw header lines (reversed to match headers array order)
message.headerLines = (this.root.rawHeaderLines || []).slice().reverse();
switch (this.attachmentEncoding) {
case 'arraybuffer':
break;
case 'base64':
for (let attachment of message.attachments || []) {
if (attachment?.content) {
attachment.content = base64ArrayBuffer(attachment.content);
attachment.encoding = 'base64';
}
}
break;
case 'utf8':
let attachmentDecoder = new TextDecoder('utf8');
for (let attachment of message.attachments || []) {
if (attachment?.content) {
attachment.content = attachmentDecoder.decode(attachment.content);
attachment.encoding = 'utf8';
}
}
break;
default:
throw new Error('Unknown attachment encoding');
}
return message;
}
}

122
node_modules/postal-mime/src/qp-decoder.js generated vendored Normal file
View File

@@ -0,0 +1,122 @@
import { blobToArrayBuffer } from './decode-strings.js';
// Regex patterns compiled once for performance
const VALID_QP_REGEX = /^=[a-f0-9]{2}$/i;
const QP_SPLIT_REGEX = /(?==[a-f0-9]{2})/i;
const SOFT_LINE_BREAK_REGEX = /=\r?\n/g;
const PARTIAL_QP_ENDING_REGEX = /=[a-fA-F0-9]?$/;
export default class QPDecoder {
constructor(opts) {
opts = opts || {};
this.decoder = opts.decoder || new TextDecoder();
this.maxChunkSize = 100 * 1024;
this.remainder = '';
this.chunks = [];
}
decodeQPBytes(encodedBytes) {
let buf = new ArrayBuffer(encodedBytes.length);
let dataView = new DataView(buf);
for (let i = 0, len = encodedBytes.length; i < len; i++) {
dataView.setUint8(i, parseInt(encodedBytes[i], 16));
}
return buf;
}
decodeChunks(str) {
// unwrap newlines
str = str.replace(SOFT_LINE_BREAK_REGEX, '');
let list = str.split(QP_SPLIT_REGEX);
let encodedBytes = [];
for (let part of list) {
if (part.charAt(0) !== '=') {
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
}
this.chunks.push(part);
continue;
}
if (part.length === 3) {
// Validate that this is actually a valid QP sequence
if (VALID_QP_REGEX.test(part)) {
encodedBytes.push(part.substr(1));
} else {
// Not a valid QP sequence, treat as literal text
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
}
this.chunks.push(part);
}
continue;
}
if (part.length > 3) {
// First 3 chars should be a valid QP sequence
const firstThree = part.substr(0, 3);
if (VALID_QP_REGEX.test(firstThree)) {
encodedBytes.push(part.substr(1, 2));
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
part = part.substr(3);
this.chunks.push(part);
} else {
// Not a valid QP sequence, treat entire part as literal
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
encodedBytes = [];
}
this.chunks.push(part);
}
}
}
if (encodedBytes.length) {
this.chunks.push(this.decodeQPBytes(encodedBytes));
}
}
update(buffer) {
// expect full lines, so add line terminator as well
let str = this.decoder.decode(buffer) + '\n';
str = this.remainder + str;
if (str.length < this.maxChunkSize) {
this.remainder = str;
return;
}
this.remainder = '';
let partialEnding = str.match(PARTIAL_QP_ENDING_REGEX);
if (partialEnding) {
if (partialEnding.index === 0) {
this.remainder = str;
return;
}
this.remainder = str.substr(partialEnding.index);
str = str.substr(0, partialEnding.index);
}
this.decodeChunks(str);
}
finalize() {
if (this.remainder.length) {
this.decodeChunks(this.remainder);
this.remainder = '';
}
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
}
}

351
node_modules/postal-mime/src/text-format.js generated vendored Normal file
View File

@@ -0,0 +1,351 @@
import htmlEntities from './html-entities.js';
export function decodeHTMLEntities(str) {
return str.replace(/&(#\d+|#x[a-f0-9]+|[a-z]+\d*);?/gi, (match, entity) => {
if (typeof htmlEntities[match] === 'string') {
return htmlEntities[match];
}
if (entity.charAt(0) !== '#' || match.charAt(match.length - 1) !== ';') {
// keep as is, invalid or unknown sequence
return match;
}
let codePoint;
if (entity.charAt(1) === 'x') {
// hex
codePoint = parseInt(entity.substr(2), 16);
} else {
// dec
codePoint = parseInt(entity.substr(1), 10);
}
let output = '';
if ((codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
// Invalid range, return a replacement character instead
return '\uFFFD';
}
if (codePoint > 0xffff) {
codePoint -= 0x10000;
output += String.fromCharCode(((codePoint >>> 10) & 0x3ff) | 0xd800);
codePoint = 0xdc00 | (codePoint & 0x3ff);
}
output += String.fromCharCode(codePoint);
return output;
});
}
export function escapeHtml(str) {
return str.trim().replace(/[<>"'?&]/g, c => {
let hex = c.charCodeAt(0).toString(16);
if (hex.length < 2) {
hex = '0' + hex;
}
return '&#x' + hex.toUpperCase() + ';';
});
}
export function textToHtml(str) {
let html = escapeHtml(str).replace(/\n/g, '<br />');
return '<div>' + html + '</div>';
}
export function htmlToText(str) {
str = str
// we can't process tags on multiple lines so remove newlines first
.replace(/\r?\n/g, '\u0001')
.replace(/<\!\-\-.*?\-\->/gi, ' ')
.replace(/<br\b[^>]*>/gi, '\n')
.replace(/<\/?(p|div|table|tr|td|th)\b[^>]*>/gi, '\n\n')
.replace(/<script\b[^>]*>.*?<\/script\b[^>]*>/gi, ' ')
.replace(/^.*<body\b[^>]*>/i, '')
.replace(/^.*<\/head\b[^>]*>/i, '')
.replace(/^.*<\!doctype\b[^>]*>/i, '')
.replace(/<\/body\b[^>]*>.*$/i, '')
.replace(/<\/html\b[^>]*>.*$/i, '')
.replace(/<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/gi, ' ($1) ')
.replace(/<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, '')
.replace(/<li\b[^>]*>[\n\u0001\s]*/gi, '* ')
.replace(/<hr\b[^>]*>/g, '\n-------------\n')
.replace(/<[^>]*>/g, ' ')
// convert linebreak placeholders back to newlines
.replace(/\u0001/g, '\n')
.replace(/[ \t]+/g, ' ')
.replace(/^\s+$/gm, '')
.replace(/\n\n+/g, '\n\n')
.replace(/^\n+/, '\n')
.replace(/\n+$/, '\n');
str = decodeHTMLEntities(str);
return str;
}
function formatTextAddress(address) {
return []
.concat(address.name || [])
.concat(address.name ? `<${address.address}>` : address.address)
.join(' ');
}
function formatTextAddresses(addresses) {
let parts = [];
let processAddress = (address, partCounter) => {
if (partCounter) {
parts.push(', ');
}
if (address.group) {
let groupStart = `${address.name}:`;
let groupEnd = `;`;
parts.push(groupStart);
address.group.forEach(processAddress);
parts.push(groupEnd);
} else {
parts.push(formatTextAddress(address));
}
};
addresses.forEach(processAddress);
return parts.join('');
}
function formatHtmlAddress(address) {
return `<a href="mailto:${escapeHtml(address.address)}" class="postal-email-address">${escapeHtml(address.name || `<${address.address}>`)}</a>`;
}
function formatHtmlAddresses(addresses) {
let parts = [];
let processAddress = (address, partCounter) => {
if (partCounter) {
parts.push('<span class="postal-email-address-separator">, </span>');
}
if (address.group) {
let groupStart = `<span class="postal-email-address-group">${escapeHtml(address.name)}:</span>`;
let groupEnd = `<span class="postal-email-address-group">;</span>`;
parts.push(groupStart);
address.group.forEach(processAddress);
parts.push(groupEnd);
} else {
parts.push(formatHtmlAddress(address));
}
};
addresses.forEach(processAddress);
return parts.join(' ');
}
function foldLines(str, lineLength, afterSpace) {
str = (str || '').toString();
lineLength = lineLength || 76;
let pos = 0,
len = str.length,
result = '',
line,
match;
while (pos < len) {
line = str.substr(pos, lineLength);
if (line.length < lineLength) {
result += line;
break;
}
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
line = match[0];
result += line;
pos += line.length;
continue;
} else if (
(match = line.match(/(\s+)[^\s]*$/)) &&
match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length
) {
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
}
result += line;
pos += line.length;
if (pos < len) {
result += '\r\n';
}
}
return result;
}
export function formatTextHeader(message) {
let rows = [];
if (message.from) {
rows.push({ key: 'From', val: formatTextAddress(message.from) });
}
if (message.subject) {
rows.push({ key: 'Subject', val: message.subject });
}
if (message.date) {
let dateOptions = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
};
let dateStr =
typeof Intl === 'undefined'
? message.date
: new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
rows.push({ key: 'Date', val: dateStr });
}
if (message.to && message.to.length) {
rows.push({ key: 'To', val: formatTextAddresses(message.to) });
}
if (message.cc && message.cc.length) {
rows.push({ key: 'Cc', val: formatTextAddresses(message.cc) });
}
if (message.bcc && message.bcc.length) {
rows.push({ key: 'Bcc', val: formatTextAddresses(message.bcc) });
}
// Align keys and values by adding space between these two
// Also make sure that the separator line is as long as the longest line
// Should end up with something like this:
/*
-----------------------------
From: xx xx <xxx@xxx.com>
Subject: Example Subject
Date: 16/02/2021, 02:57:06
To: not@found.com
-----------------------------
*/
let maxKeyLength = rows
.map(r => r.key.length)
.reduce((acc, cur) => {
return cur > acc ? cur : acc;
}, 0);
rows = rows.flatMap(row => {
let sepLen = maxKeyLength - row.key.length;
let prefix = `${row.key}: ${' '.repeat(sepLen)}`;
let emptyPrefix = `${' '.repeat(row.key.length + 1)} ${' '.repeat(sepLen)}`;
let foldedLines = foldLines(row.val, 80, true)
.split(/\r?\n/)
.map(line => line.trim());
return foldedLines.map((line, i) => `${i ? emptyPrefix : prefix}${line}`);
});
let maxLineLength = rows
.map(r => r.length)
.reduce((acc, cur) => {
return cur > acc ? cur : acc;
}, 0);
let lineMarker = '-'.repeat(maxLineLength);
let template = `
${lineMarker}
${rows.join('\n')}
${lineMarker}
`;
return template;
}
export function formatHtmlHeader(message) {
let rows = [];
if (message.from) {
rows.push(
`<div class="postal-email-header-key">From</div><div class="postal-email-header-value">${formatHtmlAddress(message.from)}</div>`
);
}
if (message.subject) {
rows.push(
`<div class="postal-email-header-key">Subject</div><div class="postal-email-header-value postal-email-header-subject">${escapeHtml(
message.subject
)}</div>`
);
}
if (message.date) {
let dateOptions = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
};
let dateStr =
typeof Intl === 'undefined'
? message.date
: new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
rows.push(
`<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(
message.date
)}">${escapeHtml(dateStr)}</div>`
);
}
if (message.to && message.to.length) {
rows.push(
`<div class="postal-email-header-key">To</div><div class="postal-email-header-value">${formatHtmlAddresses(message.to)}</div>`
);
}
if (message.cc && message.cc.length) {
rows.push(
`<div class="postal-email-header-key">Cc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.cc)}</div>`
);
}
if (message.bcc && message.bcc.length) {
rows.push(
`<div class="postal-email-header-key">Bcc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.bcc)}</div>`
);
}
let template = `<div class="postal-email-header">${rows.length ? '<div class="postal-email-header-row">' : ''}${rows.join(
'</div>\n<div class="postal-email-header-row">'
)}${rows.length ? '</div>' : ''}</div>`;
return template;
}