FRE-709: Document duplicate recovery wake - FRE-635 already recovered via FRE-708
This commit is contained in:
276
node_modules/postal-mime/CHANGELOG.md
generated
vendored
Normal file
276
node_modules/postal-mime/CHANGELOG.md
generated
vendored
Normal 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
16
node_modules/postal-mime/LICENSE.txt
generated
vendored
Normal 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
427
node_modules/postal-mime/README.md
generated
vendored
Normal 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
|
||||
|
||||
© 2021–2026 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
325
node_modules/postal-mime/dist/address-parser.cjs
generated
vendored
Normal 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
75
node_modules/postal-mime/dist/base64-decoder.cjs
generated
vendored
Normal 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
75
node_modules/postal-mime/dist/base64-encoder.cjs
generated
vendored
Normal 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
257
node_modules/postal-mime/dist/decode-strings.cjs
generated
vendored
Normal 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
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
327
node_modules/postal-mime/dist/mime-node.cjs
generated
vendored
Normal 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
53
node_modules/postal-mime/dist/pass-through-decoder.cjs
generated
vendored
Normal 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
496
node_modules/postal-mime/dist/postal-mime.cjs
generated
vendored
Normal 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
134
node_modules/postal-mime/dist/qp-decoder.cjs
generated
vendored
Normal 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
284
node_modules/postal-mime/dist/text-format.cjs
generated
vendored
Normal 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
51
node_modules/postal-mime/package.json
generated
vendored
Normal 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
94
node_modules/postal-mime/postal-mime.d.ts
generated
vendored
Normal 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
395
node_modules/postal-mime/src/address-parser.js
generated
vendored
Normal 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
48
node_modules/postal-mime/src/base64-decoder.js
generated
vendored
Normal 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
69
node_modules/postal-mime/src/base64-encoder.js
generated
vendored
Normal 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
287
node_modules/postal-mime/src/decode-strings.js
generated
vendored
Normal 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
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
373
node_modules/postal-mime/src/mime-node.js
generated
vendored
Normal 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
3
node_modules/postal-mime/src/package.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
17
node_modules/postal-mime/src/pass-through-decoder.js
generated
vendored
Normal file
17
node_modules/postal-mime/src/pass-through-decoder.js
generated
vendored
Normal 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
584
node_modules/postal-mime/src/postal-mime.js
generated
vendored
Normal 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
122
node_modules/postal-mime/src/qp-decoder.js
generated
vendored
Normal 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
351
node_modules/postal-mime/src/text-format.js
generated
vendored
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user