@ni-c/imap-mcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -31
- package/dist/analyze.d.ts +22 -3
- package/dist/analyze.js +202 -23
- package/dist/analyze.js.map +1 -1
- package/dist/attachments.d.ts +25 -0
- package/dist/attachments.js +20 -2
- package/dist/attachments.js.map +1 -1
- package/dist/audit.d.ts +7 -0
- package/dist/audit.js +11 -3
- package/dist/audit.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +44 -5
- package/dist/config.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/imap.js.map +1 -1
- package/dist/index.js +32 -5
- package/dist/index.js.map +1 -1
- package/dist/output-schema.d.ts +62 -0
- package/dist/output-schema.js +81 -0
- package/dist/output-schema.js.map +1 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +1 -1
- package/dist/resources.js.map +1 -1
- package/dist/result.d.ts +32 -5
- package/dist/result.js +128 -24
- package/dist/result.js.map +1 -1
- package/dist/schema.d.ts +13 -1
- package/dist/schema.js +20 -2
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +30 -5
- package/dist/server.js.map +1 -1
- package/dist/tools/annotations.d.ts +32 -0
- package/dist/tools/annotations.js +33 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/catalogue.d.ts +2 -2
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +345 -50
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +3 -3
- package/dist/tools/write.js +165 -38
- package/dist/tools/write.js.map +1 -1
- package/package.json +15 -11
- package/dist/approval.d.ts +0 -45
- package/dist/approval.js +0 -69
- package/dist/approval.js.map +0 -1
- package/dist/confirm.d.ts +0 -59
- package/dist/confirm.js +0 -92
- package/dist/confirm.js.map +0 -1
- package/dist/tool-filter.d.ts +0 -45
- package/dist/tool-filter.js +0 -171
- package/dist/tool-filter.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://github.com/ni-c/imap-mcp/pkgs/container/imap-mcp)
|
|
9
9
|
[](https://imap-mcp.ni-c.de)
|
|
10
|
+
[](https://mcp-hub.ni-c.de)
|
|
10
11
|
[](https://github.com/sponsors/ni-c)
|
|
11
12
|
|
|
12
13
|
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for any IMAP
|
|
@@ -63,6 +64,11 @@ dropped on a best-effort basis (the fencing, not the stripping, is what carries
|
|
|
63
64
|
markdown image syntax — inline and reference style — is defused so a rendering client cannot be
|
|
64
65
|
made to fetch a tracking URL.
|
|
65
66
|
|
|
67
|
+
That covers folder names too, and it did not always: a folder name is chosen by whoever created
|
|
68
|
+
the folder, which on a shared mailbox is not necessarily you. `list_mailboxes` returns the name
|
|
69
|
+
twice — `path` exactly as the server spelled it, because that is the handle every other tool
|
|
70
|
+
takes, and `display_name` cleaned up for reading, with a warning on the entry when the two differ.
|
|
71
|
+
|
|
66
72
|
Alongside the message you get a server-side assessment: the SPF/DKIM/DMARC verdicts with the
|
|
67
73
|
authserv-id they came from, which prompt-injection shapes matched, and which words mix Latin
|
|
68
74
|
with Cyrillic or Greek letters. When something matches, the warning is the first thing in the
|
|
@@ -79,9 +85,11 @@ is reported as what it is: a claim, from a header anyone could have written.
|
|
|
79
85
|
IMAP keyword (`AiSeen` by default), so `list_new_messages` returns each message once. The human
|
|
80
86
|
`\Seen` state is never touched — everything is read with `BODY.PEEK`.
|
|
81
87
|
|
|
82
|
-
**Deleting
|
|
83
|
-
deleting a folder raise a real dialog that the model cannot answer on its
|
|
84
|
-
not, they fall back to a two-call token — and say so, rather than implying
|
|
88
|
+
**Deleting and moving ask a person.** Where the client supports MCP elicitation, `delete_messages`,
|
|
89
|
+
`move_messages` and deleting a folder raise a real dialog that the model cannot answer on its
|
|
90
|
+
behalf. Where it does not, they fall back to a two-call token — and say so, rather than implying
|
|
91
|
+
somebody approved. `ELICITATION=false` takes that fallback deliberately; it never removes the
|
|
92
|
+
guard. See [Asking a person](https://imap-mcp.ni-c.de/guide/approval).
|
|
85
93
|
|
|
86
94
|
## Requirements
|
|
87
95
|
|
|
@@ -91,26 +99,27 @@ not, they fall back to a two-call token — and say so, rather than implying som
|
|
|
91
99
|
|
|
92
100
|
## Configuration
|
|
93
101
|
|
|
94
|
-
| Variable | Required | Default | Description
|
|
95
|
-
| --------------------------- | -------- | ------------- |
|
|
96
|
-
| `IMAP_HOST` | yes | — | Hostname of the IMAP server, e.g. `imap.example.net`
|
|
97
|
-
| `IMAP_USER` | yes | — | Account username, usually the address
|
|
98
|
-
| `IMAP_PASSWORD` | yes | — | Password or app-specific password
|
|
99
|
-
| `IMAP_PORT` | no | `993` / `143` | Defaults by TLS mode
|
|
100
|
-
| `IMAP_TLS` | no | `implicit` | `implicit`, `starttls` or `none`
|
|
101
|
-
| `IMAP_MAILBOX` | no | `INBOX` | Mailbox the message tools default to
|
|
102
|
-
| `IMAP_READ_ONLY` | no | **`true`** | Exactly `false` registers the five mailbox tools
|
|
103
|
-
| `IMAP_ALLOW_TOOLS` | no | — | Tool names, `list_*` prefixes or `essential`
|
|
104
|
-
| `IMAP_DENY_TOOLS` | no | — | Same syntax; subtracted from the allow list
|
|
105
|
-
| `IMAP_SEEN_KEYWORD` | no | `AiSeen` | Keyword for new-mail tracking; empty turns it off
|
|
106
|
-
| `IMAP_TRUSTED_AUTHSERV_ID` | no | — | The authserv-id your provider stamps; see below
|
|
107
|
-
| `IMAP_DRAFTS_MAILBOX` | no | auto | Overrides the folder found via the `\Drafts` flag
|
|
108
|
-
| `IMAP_MAX_MESSAGES` | no | `100` | Default page size
|
|
109
|
-
| `IMAP_MAX_ATTACHMENT_BYTES` | no | `1048576` | Ceiling for returning an attachment inline
|
|
110
|
-
| `IMAP_MAX_DOWNLOAD_BYTES` | no | `26214400` | Ceiling for writing one to disk
|
|
111
|
-
| `IMAP_ATTACHMENT_TYPES` | no | see below | Comma-separated content-type allowlist
|
|
112
|
-
| `IMAP_DOWNLOAD_DIR` | no | — | Setting it allows saving attachments there
|
|
113
|
-
| `IMAP_INSECURE_TLS` | no | `false` | Exactly `true` accepts a self-signed certificate
|
|
102
|
+
| Variable | Required | Default | Description |
|
|
103
|
+
| --------------------------- | -------- | ------------- | ------------------------------------------------------------ |
|
|
104
|
+
| `IMAP_HOST` | yes | — | Hostname of the IMAP server, e.g. `imap.example.net` |
|
|
105
|
+
| `IMAP_USER` | yes | — | Account username, usually the address |
|
|
106
|
+
| `IMAP_PASSWORD` | yes | — | Password or app-specific password |
|
|
107
|
+
| `IMAP_PORT` | no | `993` / `143` | Defaults by TLS mode |
|
|
108
|
+
| `IMAP_TLS` | no | `implicit` | `implicit`, `starttls` or `none` |
|
|
109
|
+
| `IMAP_MAILBOX` | no | `INBOX` | Mailbox the message tools default to |
|
|
110
|
+
| `IMAP_READ_ONLY` | no | **`true`** | Exactly `false` registers the five mailbox tools |
|
|
111
|
+
| `IMAP_ALLOW_TOOLS` | no | — | Tool names, `list_*` prefixes or `essential` |
|
|
112
|
+
| `IMAP_DENY_TOOLS` | no | — | Same syntax; subtracted from the allow list |
|
|
113
|
+
| `IMAP_SEEN_KEYWORD` | no | `AiSeen` | Keyword for new-mail tracking; empty turns it off |
|
|
114
|
+
| `IMAP_TRUSTED_AUTHSERV_ID` | no | — | The authserv-id your provider stamps; see below |
|
|
115
|
+
| `IMAP_DRAFTS_MAILBOX` | no | auto | Overrides the folder found via the `\Drafts` flag |
|
|
116
|
+
| `IMAP_MAX_MESSAGES` | no | `100` | Default page size |
|
|
117
|
+
| `IMAP_MAX_ATTACHMENT_BYTES` | no | `1048576` | Ceiling for returning an attachment inline |
|
|
118
|
+
| `IMAP_MAX_DOWNLOAD_BYTES` | no | `26214400` | Ceiling for writing one to disk |
|
|
119
|
+
| `IMAP_ATTACHMENT_TYPES` | no | see below | Comma-separated content-type allowlist |
|
|
120
|
+
| `IMAP_DOWNLOAD_DIR` | no | — | Setting it allows saving attachments there |
|
|
121
|
+
| `IMAP_INSECURE_TLS` | no | `false` | Exactly `true` accepts a self-signed certificate |
|
|
122
|
+
| `ELICITATION` | no | `true` | `false` replaces the dialog with the token. **Not prefixed** |
|
|
114
123
|
|
|
115
124
|
Booleans are compared against the literal string `true`; `1`, `yes` and `True` are not true.
|
|
116
125
|
`IMAP_READ_ONLY` is the mirror image: only the literal `false` turns it off, so a typo leaves
|
|
@@ -204,6 +213,41 @@ bind mount has to be owned by it on the host: `-e IMAP_DOWNLOAD_DIR=/data -v
|
|
|
204
213
|
"$PWD/attachments:/data"` with `chown 1000:1000 attachments`. Without
|
|
205
214
|
`IMAP_DOWNLOAD_DIR` the container never writes anything.
|
|
206
215
|
|
|
216
|
+
### Through mcp-hub
|
|
217
|
+
|
|
218
|
+
A client that cannot spawn a local process — ChatGPT connectors, Claude on the web,
|
|
219
|
+
Cursor, LibreChat — reaches imap-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one
|
|
220
|
+
container serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login
|
|
221
|
+
behind a single password and long-lived tokens for the clients that cannot do OAuth. Its
|
|
222
|
+
`/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of
|
|
223
|
+
them without N×tool schemas in the model's context, and it speaks both protocol revisions
|
|
224
|
+
— a question this server asks travels through it to the person at the far end.
|
|
225
|
+
|
|
226
|
+
Its `/config/mcp.json` uses Claude Code's format, so the entry is the one you already
|
|
227
|
+
have:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"mcpServers": {
|
|
232
|
+
"imap-mcp": {
|
|
233
|
+
"command": "npx",
|
|
234
|
+
"args": ["-y", "@ni-c/imap-mcp"],
|
|
235
|
+
"env": {
|
|
236
|
+
"IMAP_HOST": "imap.example.net",
|
|
237
|
+
"IMAP_USER": "me@example.net",
|
|
238
|
+
"IMAP_PASSWORD": "…",
|
|
239
|
+
"IMAP_ALLOW_TOOLS": "essential"
|
|
240
|
+
},
|
|
241
|
+
"denyTools": ["delete_messages"]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not
|
|
248
|
+
the same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,
|
|
249
|
+
are in the [client guide](https://imap-mcp.ni-c.de/guide/clients#through-mcp-hub).
|
|
250
|
+
|
|
207
251
|
## Tools
|
|
208
252
|
|
|
209
253
|
**Read** — always registered
|
|
@@ -219,13 +263,13 @@ bind mount has to be owned by it on the host: `-e IMAP_DOWNLOAD_DIR=/data -v
|
|
|
219
263
|
|
|
220
264
|
**Mailbox** — needs `IMAP_READ_ONLY=false`
|
|
221
265
|
|
|
222
|
-
| Tool | Confirmation
|
|
223
|
-
| ------------------- |
|
|
224
|
-
| `set_message_flags` | none — flags are reversible, and `\Deleted` is refused
|
|
225
|
-
| `move_messages` |
|
|
226
|
-
| `delete_messages` | 👤 asks the user, 🔒 where the client cannot
|
|
227
|
-
| `manage_mailbox` | 👤 for `delete`, 🔒 for `rename`, none for `create`
|
|
228
|
-
| `save_draft` | none — a draft does not leave the mailbox
|
|
266
|
+
| Tool | Confirmation |
|
|
267
|
+
| ------------------- | --------------------------------------------------------- |
|
|
268
|
+
| `set_message_flags` | none — flags are reversible, and `\Deleted` is refused |
|
|
269
|
+
| `move_messages` | 👤 for both `move` and `copy`, 🔒 where the client cannot |
|
|
270
|
+
| `delete_messages` | 👤 asks the user, 🔒 where the client cannot |
|
|
271
|
+
| `manage_mailbox` | 👤 for `delete`, 🔒 for `rename`, none for `create` |
|
|
272
|
+
| `save_draft` | none — a draft does not leave the mailbox |
|
|
229
273
|
|
|
230
274
|
👤 raises a dialog the model cannot answer · 🔒 needs a confirmation token: call once to
|
|
231
275
|
receive one, then again with it.
|
|
@@ -240,6 +284,41 @@ it. Use `delete_messages`, which asks.
|
|
|
240
284
|
Neither a confirmation nor a dialog quotes a mailbox name inside its own sentence — folder
|
|
241
285
|
names come from the account, which on a shared mailbox means a colleague chose them.
|
|
242
286
|
|
|
287
|
+
### Structured output
|
|
288
|
+
|
|
289
|
+
Every tool declares an `outputSchema` and answers with `structuredContent`
|
|
290
|
+
alongside the text block, so a client can use the result without parsing prose:
|
|
291
|
+
|
|
292
|
+
```jsonc
|
|
293
|
+
{
|
|
294
|
+
"untrusted": true,
|
|
295
|
+
"source": "imap",
|
|
296
|
+
"mailbox": "INBOX",
|
|
297
|
+
"total_matching": 214,
|
|
298
|
+
"offset": 0,
|
|
299
|
+
"returned": 25,
|
|
300
|
+
"next_offset": 25,
|
|
301
|
+
"messages": [{ "uid": 4711, "subject": "…", "from": "…", "seen": false }],
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Every tool that reports anything out of the mailbox carries `untrusted: true`
|
|
306
|
+
and `source: "imap"` as fields — a sender display name, a folder name a
|
|
307
|
+
colleague chose and an attachment filename are all attacker-controllable, and
|
|
308
|
+
they reach the model through the listing tools long before anyone opens a
|
|
309
|
+
message. Only `get_server_info` and the five write tools are without it: those
|
|
310
|
+
report this server's own configuration, or what it just did with the uids it was
|
|
311
|
+
given.
|
|
312
|
+
|
|
313
|
+
`get_message` and a text attachment keep the per-call nonce fence in the text
|
|
314
|
+
block — the structured half states the same fields, so a client is not made to
|
|
315
|
+
parse the fence to find them. An image attachment keeps its bytes in the content
|
|
316
|
+
block, where a client renders them, rather than repeating the base64.
|
|
317
|
+
|
|
318
|
+
A refusal is now an **error result**: an attachment the policy rejects, one whose
|
|
319
|
+
bytes are an executable whatever it claimed, one too large to inline. Each was a
|
|
320
|
+
plain result that read like an answer.
|
|
321
|
+
|
|
243
322
|
Attachments are also available as MCP resources at `imap://message/{uid}/part/{partId}`, which
|
|
244
323
|
matters where the server has no useful filesystem. The resource path runs the same allowlist,
|
|
245
324
|
size limit and magic-byte check as the tool — it is not a second, unguarded door.
|
|
@@ -282,6 +361,11 @@ knowing before concluding that a filtered install reaches less of the mailbox th
|
|
|
282
361
|
`SECURITY.md` has the trust model, what these measures do _not_ cover, and how to report a
|
|
283
362
|
vulnerability.
|
|
284
363
|
|
|
364
|
+
## Documentation
|
|
365
|
+
|
|
366
|
+
The full guide, tool reference and security notes live at
|
|
367
|
+
**[imap-mcp.ni-c.de](https://imap-mcp.ni-c.de)** (source in [`docs/`](docs/)).
|
|
368
|
+
|
|
285
369
|
## Development
|
|
286
370
|
|
|
287
371
|
```bash
|
|
@@ -303,6 +387,13 @@ network. For a live server to point the real thing at, see
|
|
|
303
387
|
The release workflow publishes to npm (Trusted Publishing, with provenance), creates
|
|
304
388
|
the GitHub release from the CHANGELOG section and updates the MCP Registry entry.
|
|
305
389
|
|
|
390
|
+
## Contributing
|
|
391
|
+
|
|
392
|
+
Issues, discussions and pull requests are welcome — see
|
|
393
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use
|
|
394
|
+
[private reporting](https://github.com/ni-c/imap-mcp/security/advisories/new)
|
|
395
|
+
rather than a public issue; the policy is in [SECURITY.md](SECURITY.md).
|
|
396
|
+
|
|
306
397
|
## License
|
|
307
398
|
|
|
308
|
-
MIT © Willi Thiel
|
|
399
|
+
[MIT](LICENSE) © Willi Thiel
|
package/dist/analyze.d.ts
CHANGED
|
@@ -28,9 +28,17 @@ export interface SecurityAssessment {
|
|
|
28
28
|
* Deliberately not `mailparser`'s own `text` fallback: that keeps content the
|
|
29
29
|
* recipient never sees. Anything hidden by inline CSS is a place to park an
|
|
30
30
|
* instruction meant only for the model, so those elements are dropped before
|
|
31
|
-
* the tags are stripped.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* the tags are stripped.
|
|
32
|
+
*
|
|
33
|
+
* This is one pass over the input. The cursors below only ever move forward,
|
|
34
|
+
* which is the property that makes the whole function linear no matter what the
|
|
35
|
+
* sender writes: a start token that is never closed is answered once and then
|
|
36
|
+
* never looked for again, instead of restarting a bounded scan at every
|
|
37
|
+
* occurrence. Removal itself stays best effort — nested same-name tags end a
|
|
38
|
+
* block early, elements hidden via a stylesheet class are not recognised at
|
|
39
|
+
* all, and anything past the scan budget is left in place. That is acceptable
|
|
40
|
+
* for the same reason it always was: nothing downstream trusts the stripping,
|
|
41
|
+
* and the fencing in {@link wrapUntrusted} is what carries the weight.
|
|
34
42
|
*/
|
|
35
43
|
export declare function htmlToText(html: string): string;
|
|
36
44
|
/**
|
|
@@ -40,6 +48,17 @@ export declare function htmlToText(html: string): string;
|
|
|
40
48
|
* body, because it is rendered next to one and read with the same eyes.
|
|
41
49
|
*/
|
|
42
50
|
export declare function stripInvisible(input: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The same characters, written out as escapes instead of removed.
|
|
53
|
+
*
|
|
54
|
+
* For the places where a string has to stay recognisable as the exact thing
|
|
55
|
+
* that was asked for — a confirmation dialog, an audit line. Stripping alone
|
|
56
|
+
* says "this is not what it looked like" and then shows something that looks
|
|
57
|
+
* like an ordinary name; this shows which characters were in it, so a person
|
|
58
|
+
* deciding whether to move mail into `Archive<U+202E>` can see that the folder is
|
|
59
|
+
* not the `Archive` they know.
|
|
60
|
+
*/
|
|
61
|
+
export declare function escapeInvisible(input: string): string;
|
|
43
62
|
/**
|
|
44
63
|
* Normalises text before it reaches the model: Unicode-folded, stripped of the
|
|
45
64
|
* characters a human reader cannot see, auto-fetch markup defused, and
|
package/dist/analyze.js
CHANGED
|
@@ -86,46 +86,211 @@ const INJECTION_PATTERNS = [
|
|
|
86
86
|
],
|
|
87
87
|
];
|
|
88
88
|
/**
|
|
89
|
-
* Cap on the HTML
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
89
|
+
* Cap on the HTML this pass looks at, and on the span a single element may
|
|
90
|
+
* swallow.
|
|
91
|
+
*
|
|
92
|
+
* Neither of these is what keeps the pass cheap, and the previous version of
|
|
93
|
+
* this comment claimed otherwise. Removing an element means scanning forward
|
|
94
|
+
* for a closing token, and a bound on *that* scan bounds one factor of a
|
|
95
|
+
* product whose other factor is the number of scans an input can start. A body
|
|
96
|
+
* of `'<style '` repeated 73 000 times is 512 000 legal bytes that start 73 000
|
|
97
|
+
* bounded scans and finish none of them: the removal regexes this used to be
|
|
98
|
+
* built from took 33 seconds on it, on a single-threaded process whose
|
|
99
|
+
* transport is stdio. The scan below is a single left-to-right walk instead, so
|
|
100
|
+
* the number of start tokens no longer multiplies anything.
|
|
97
101
|
*/
|
|
98
102
|
const MAX_HTML_CHARS = 512_000;
|
|
99
103
|
const MAX_REMOVED_BLOCK_CHARS = 50_000;
|
|
100
104
|
const MAX_HIDDEN_ELEMENT_CHARS = 10_000;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Total characters the walk may spend looking for closing tags, across the
|
|
107
|
+
* whole document.
|
|
108
|
+
*
|
|
109
|
+
* This is the cap on the product. Each successful removal costs the length of
|
|
110
|
+
* what it removed, and removals do not overlap, so an honest document never
|
|
111
|
+
* comes close; only an input that starts removals it never closes can exhaust
|
|
112
|
+
* it. Once it is gone the remaining elements are stripped as ordinary tags and
|
|
113
|
+
* their content stays visible — best effort degrading to less effort, never to
|
|
114
|
+
* a stalled server.
|
|
115
|
+
*/
|
|
116
|
+
const CLOSER_SCAN_BUDGET_FACTOR = 4;
|
|
117
|
+
const CLOSER_SCAN_BUDGET_FLOOR = 100_000;
|
|
118
|
+
/** Elements whose content the recipient never reads. */
|
|
119
|
+
const NON_CONTENT_TAGS = new Set([
|
|
120
|
+
'script',
|
|
121
|
+
'style',
|
|
122
|
+
'head',
|
|
123
|
+
'title',
|
|
124
|
+
'noscript',
|
|
125
|
+
'template',
|
|
126
|
+
]);
|
|
127
|
+
/** Closing tags that end a visual block, and so earn a line break. */
|
|
128
|
+
const BLOCK_TAGS = new Set([
|
|
129
|
+
'p',
|
|
130
|
+
'div',
|
|
131
|
+
'tr',
|
|
132
|
+
'li',
|
|
133
|
+
'h1',
|
|
134
|
+
'h2',
|
|
135
|
+
'h3',
|
|
136
|
+
'h4',
|
|
137
|
+
'h5',
|
|
138
|
+
'h6',
|
|
139
|
+
]);
|
|
140
|
+
const TAG_NAME = /^<\/?([A-Za-z][A-Za-z0-9]*)/;
|
|
141
|
+
const BR_TAG = /^<br\s*\/?$/i;
|
|
142
|
+
const STYLE_ATTRIBUTE = /style\s*=\s*("|')/gi;
|
|
143
|
+
const HIDDEN_VALUE = /display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0|font-size\s*:\s*0/i;
|
|
144
|
+
/**
|
|
145
|
+
* Whether a start tag carries an inline style that hides it.
|
|
146
|
+
*
|
|
147
|
+
* Written as "find the attribute, then look inside its value" rather than as
|
|
148
|
+
* one pattern spanning both, because the one-pattern form has to guess where
|
|
149
|
+
* the value ends and backtrack when it guesses wrong. Here each style value is
|
|
150
|
+
* inspected once and the walk over the tag never turns around.
|
|
151
|
+
*/
|
|
152
|
+
function hasHiddenStyle(tag) {
|
|
153
|
+
STYLE_ATTRIBUTE.lastIndex = 0;
|
|
154
|
+
for (let match = STYLE_ATTRIBUTE.exec(tag); match !== null; match = STYLE_ATTRIBUTE.exec(tag)) {
|
|
155
|
+
const quote = match[1];
|
|
156
|
+
const start = match.index + match[0].length;
|
|
157
|
+
const end = tag.indexOf(quote, start);
|
|
158
|
+
if (end < 0)
|
|
159
|
+
return false;
|
|
160
|
+
if (HIDDEN_VALUE.test(tag.slice(start, end)))
|
|
161
|
+
return true;
|
|
162
|
+
STYLE_ATTRIBUTE.lastIndex = end;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
104
166
|
/**
|
|
105
167
|
* Extracts readable text from HTML.
|
|
106
168
|
*
|
|
107
169
|
* Deliberately not `mailparser`'s own `text` fallback: that keeps content the
|
|
108
170
|
* recipient never sees. Anything hidden by inline CSS is a place to park an
|
|
109
171
|
* instruction meant only for the model, so those elements are dropped before
|
|
110
|
-
* the tags are stripped.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
172
|
+
* the tags are stripped.
|
|
173
|
+
*
|
|
174
|
+
* This is one pass over the input. The cursors below only ever move forward,
|
|
175
|
+
* which is the property that makes the whole function linear no matter what the
|
|
176
|
+
* sender writes: a start token that is never closed is answered once and then
|
|
177
|
+
* never looked for again, instead of restarting a bounded scan at every
|
|
178
|
+
* occurrence. Removal itself stays best effort — nested same-name tags end a
|
|
179
|
+
* block early, elements hidden via a stylesheet class are not recognised at
|
|
180
|
+
* all, and anything past the scan budget is left in place. That is acceptable
|
|
181
|
+
* for the same reason it always was: nothing downstream trusts the stripping,
|
|
182
|
+
* and the fencing in {@link wrapUntrusted} is what carries the weight.
|
|
113
183
|
*/
|
|
114
184
|
export function htmlToText(html) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
185
|
+
const source = html.slice(0, MAX_HTML_CHARS);
|
|
186
|
+
const out = [];
|
|
187
|
+
// Forward-only cursors. Each call may advance them, never rewind them, so
|
|
188
|
+
// across the whole document each scans the input at most once — the same
|
|
189
|
+
// reason a `-->` that does not exist costs one pass rather than one per
|
|
190
|
+
// `<!--`.
|
|
191
|
+
let nextGt = source.indexOf('>');
|
|
192
|
+
let nextCommentEnd = source.indexOf('-->');
|
|
193
|
+
const gtFrom = (from) => {
|
|
194
|
+
while (nextGt >= 0 && nextGt < from)
|
|
195
|
+
nextGt = source.indexOf('>', nextGt + 1);
|
|
196
|
+
return nextGt;
|
|
197
|
+
};
|
|
198
|
+
const commentEndFrom = (from) => {
|
|
199
|
+
while (nextCommentEnd >= 0 && nextCommentEnd < from) {
|
|
200
|
+
nextCommentEnd = source.indexOf('-->', nextCommentEnd + 1);
|
|
201
|
+
}
|
|
202
|
+
return nextCommentEnd;
|
|
203
|
+
};
|
|
204
|
+
let budget = source.length * CLOSER_SCAN_BUDGET_FACTOR + CLOSER_SCAN_BUDGET_FLOOR;
|
|
205
|
+
/** Where `</name>` starts, or -1 when nothing closes this element in time. */
|
|
206
|
+
const closingTagFrom = (name, from, window) => {
|
|
207
|
+
if (budget <= 0)
|
|
208
|
+
return -1;
|
|
209
|
+
const stop = Math.min(source.length, from + window);
|
|
210
|
+
const needle = `</${name}>`;
|
|
211
|
+
let at = source.indexOf('</', from);
|
|
212
|
+
while (at >= 0 && at + needle.length <= stop) {
|
|
213
|
+
if (source.slice(at, at + needle.length).toLowerCase() === needle) {
|
|
214
|
+
budget -= at - from;
|
|
215
|
+
return at;
|
|
216
|
+
}
|
|
217
|
+
at = source.indexOf('</', at + 2);
|
|
218
|
+
}
|
|
219
|
+
// Charged against how far the search actually looked, not against the
|
|
220
|
+
// window: a document with no `</` at all sends every one of these to the
|
|
221
|
+
// end of the input, and a budget that only counted the window would let an
|
|
222
|
+
// attacker buy those scans at a fiftieth of their price.
|
|
223
|
+
budget -= (at < 0 ? source.length : Math.min(at, stop)) - from;
|
|
224
|
+
return -1;
|
|
225
|
+
};
|
|
226
|
+
let i = 0;
|
|
227
|
+
while (i < source.length) {
|
|
228
|
+
const lt = source.indexOf('<', i);
|
|
229
|
+
if (lt < 0) {
|
|
230
|
+
out.push(source.slice(i));
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (lt > i)
|
|
234
|
+
out.push(source.slice(i, lt));
|
|
235
|
+
if (source.startsWith('<!--', lt)) {
|
|
236
|
+
const end = commentEndFrom(lt + 4);
|
|
237
|
+
if (end >= 0 && end - lt <= MAX_REMOVED_BLOCK_CHARS) {
|
|
238
|
+
out.push(' ');
|
|
239
|
+
i = end + 3;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Nothing closes it anywhere ahead, so it is text that looks like
|
|
243
|
+
// markup rather than markup. Emitting it keeps the reader honest.
|
|
244
|
+
out.push('<');
|
|
245
|
+
i = lt + 1;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const gt = gtFrom(lt + 1);
|
|
250
|
+
if (gt < 0) {
|
|
251
|
+
// No `>` in the rest of the document: everything from here is text.
|
|
252
|
+
out.push(source.slice(lt));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
const tag = source.slice(lt, gt);
|
|
256
|
+
const name = TAG_NAME.exec(tag)?.[1]?.toLowerCase();
|
|
257
|
+
const closing = tag.startsWith('</');
|
|
258
|
+
if (name !== undefined && !closing) {
|
|
259
|
+
const window = NON_CONTENT_TAGS.has(name)
|
|
260
|
+
? MAX_REMOVED_BLOCK_CHARS
|
|
261
|
+
: hasHiddenStyle(tag)
|
|
262
|
+
? MAX_HIDDEN_ELEMENT_CHARS
|
|
263
|
+
: 0;
|
|
264
|
+
if (window > 0) {
|
|
265
|
+
const end = closingTagFrom(name, gt + 1, window);
|
|
266
|
+
if (end >= 0) {
|
|
267
|
+
out.push(' ');
|
|
268
|
+
i = end + name.length + 3;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (BR_TAG.test(tag)) {
|
|
274
|
+
out.push('\n');
|
|
275
|
+
}
|
|
276
|
+
else if (closing && name !== undefined && BLOCK_TAGS.has(name)) {
|
|
277
|
+
out.push('\n');
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
out.push(' ');
|
|
281
|
+
}
|
|
282
|
+
i = gt + 1;
|
|
283
|
+
}
|
|
284
|
+
return (out
|
|
285
|
+
.join('')
|
|
123
286
|
.replace(/ /gi, ' ')
|
|
124
287
|
.replace(/</gi, '<')
|
|
125
288
|
.replace(/>/gi, '>')
|
|
126
289
|
.replace(/"/gi, '"')
|
|
127
290
|
.replace(/'/g, "'")
|
|
128
|
-
|
|
291
|
+
// Last, so a decoded `&lt;` does not turn into a `<` the caller never
|
|
292
|
+
// received.
|
|
293
|
+
.replace(/&/gi, '&'));
|
|
129
294
|
}
|
|
130
295
|
/**
|
|
131
296
|
* Removes the characters a human reader cannot see but the model can.
|
|
@@ -136,6 +301,20 @@ export function htmlToText(html) {
|
|
|
136
301
|
export function stripInvisible(input) {
|
|
137
302
|
return input.replace(INVISIBLE_CHARS, '').replace(CONTROL_CHARS, '');
|
|
138
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* The same characters, written out as escapes instead of removed.
|
|
306
|
+
*
|
|
307
|
+
* For the places where a string has to stay recognisable as the exact thing
|
|
308
|
+
* that was asked for — a confirmation dialog, an audit line. Stripping alone
|
|
309
|
+
* says "this is not what it looked like" and then shows something that looks
|
|
310
|
+
* like an ordinary name; this shows which characters were in it, so a person
|
|
311
|
+
* deciding whether to move mail into `Archive<U+202E>` can see that the folder is
|
|
312
|
+
* not the `Archive` they know.
|
|
313
|
+
*/
|
|
314
|
+
export function escapeInvisible(input) {
|
|
315
|
+
const escape = (match) => `\\u${match.codePointAt(0).toString(16).padStart(4, '0')}`;
|
|
316
|
+
return input.replace(INVISIBLE_CHARS, escape).replace(CONTROL_CHARS, escape);
|
|
317
|
+
}
|
|
139
318
|
/**
|
|
140
319
|
* Normalises text before it reaches the model: Unicode-folded, stripped of the
|
|
141
320
|
* characters a human reader cannot see, auto-fetch markup defused, and
|
package/dist/analyze.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AAErC;;;;GAIG;AACH,MAAM,eAAe,GACnB,2EAA2E,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,4CAA4C;AAC5C,MAAM,aAAa,GAAG,wDAAwD,CAAC;AAE/E;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAA6C;IACnE;QACE,sBAAsB;QACtB,+HAA+H;KAChI;IACD,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E;QACE,gBAAgB;QAChB,+DAA+D;KAChE;IACD;QACE,gBAAgB;QAChB,8DAA8D;KAC/D;IACD;QACE,eAAe;QACf,6EAA6E;KAC9E;IACD;QACE,cAAc;QACd,0FAA0F;KAC3F;IACD,2EAA2E;IAC3E,wEAAwE;IACxE;QACE,oBAAoB;QACpB,4NAA4N;KAC7N;IACD;QACE,aAAa;QACb,8DAA8D;KAC/D;IACD;QACE,kBAAkB;QAClB,0FAA0F;KAC3F;IACD;QACE,gBAAgB;QAChB,wFAAwF;KACzF;IACD;QACE,aAAa;QACb,6FAA6F;KAC9F;IACD,CAAC,iBAAiB,EAAE,yCAAyC,CAAC;IAC9D;QACE,cAAc;QACd,6GAA6G;KAC9G;CACF,CAAC;AAqBF
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"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"}
|
package/dist/attachments.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { MessageStructureObject } from 'imapflow';
|
|
2
|
+
/**
|
|
3
|
+
* Extensions that are executable somewhere. The list is long on purpose: this
|
|
4
|
+
* is a refusal list applied on top of the content-type allowlist, and the cost
|
|
5
|
+
* of an extra entry is one unusual attachment that has to be fetched by other
|
|
6
|
+
* means.
|
|
7
|
+
*
|
|
8
|
+
* Exported so a test can walk it. An entry {@link extensionOf} cannot produce
|
|
9
|
+
* is not a stricter list, it is a longer one that refuses less — and nothing
|
|
10
|
+
* about reading the two declarations side by side says which is which.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EXECUTABLE_EXTENSIONS: Set<string>;
|
|
2
13
|
export interface AttachmentCandidate {
|
|
3
14
|
partId: string;
|
|
4
15
|
filename: string;
|
|
@@ -23,6 +34,20 @@ export interface AttachmentPolicy {
|
|
|
23
34
|
* separators is trying to be read as something it is not.
|
|
24
35
|
*/
|
|
25
36
|
export declare function sanitizeFilename(raw: string | undefined): string;
|
|
37
|
+
/**
|
|
38
|
+
* The trailing extension of a filename, lowercased, or `''`.
|
|
39
|
+
*
|
|
40
|
+
* The character class and the length have to cover every entry of
|
|
41
|
+
* {@link EXECUTABLE_EXTENSIONS}, or the blocklist has entries that can never be
|
|
42
|
+
* read out of a name. Both used to be too narrow: `appref-ms` carries a hyphen
|
|
43
|
+
* and `application` is eleven characters, so both returned `''` — and an empty
|
|
44
|
+
* extension makes {@link checkPolicy} skip the executable check entirely rather
|
|
45
|
+
* than fail it. `Rechnung-2026.appref-ms` declared as `application/xml` (which
|
|
46
|
+
* is in the default type allowlist, and which a ClickOnce manifest genuinely
|
|
47
|
+
* is) therefore reached the download directory under its own name with no note
|
|
48
|
+
* against it. The property test over the whole set is what keeps the two in
|
|
49
|
+
* step from here on.
|
|
50
|
+
*/
|
|
26
51
|
export declare function extensionOf(filename: string): string;
|
|
27
52
|
/**
|
|
28
53
|
* Walks the MIME tree and returns every part that is an attachment.
|
package/dist/attachments.js
CHANGED
|
@@ -9,8 +9,12 @@ const MAX_FILENAME_LENGTH = 120;
|
|
|
9
9
|
* is a refusal list applied on top of the content-type allowlist, and the cost
|
|
10
10
|
* of an extra entry is one unusual attachment that has to be fetched by other
|
|
11
11
|
* means.
|
|
12
|
+
*
|
|
13
|
+
* Exported so a test can walk it. An entry {@link extensionOf} cannot produce
|
|
14
|
+
* is not a stricter list, it is a longer one that refuses less — and nothing
|
|
15
|
+
* about reading the two declarations side by side says which is which.
|
|
12
16
|
*/
|
|
13
|
-
const EXECUTABLE_EXTENSIONS = new Set([
|
|
17
|
+
export const EXECUTABLE_EXTENSIONS = new Set([
|
|
14
18
|
'ade',
|
|
15
19
|
'adp',
|
|
16
20
|
'app',
|
|
@@ -133,8 +137,22 @@ export function sanitizeFilename(raw) {
|
|
|
133
137
|
? `${cleaned.slice(0, MAX_FILENAME_LENGTH)}…`
|
|
134
138
|
: cleaned;
|
|
135
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The trailing extension of a filename, lowercased, or `''`.
|
|
142
|
+
*
|
|
143
|
+
* The character class and the length have to cover every entry of
|
|
144
|
+
* {@link EXECUTABLE_EXTENSIONS}, or the blocklist has entries that can never be
|
|
145
|
+
* read out of a name. Both used to be too narrow: `appref-ms` carries a hyphen
|
|
146
|
+
* and `application` is eleven characters, so both returned `''` — and an empty
|
|
147
|
+
* extension makes {@link checkPolicy} skip the executable check entirely rather
|
|
148
|
+
* than fail it. `Rechnung-2026.appref-ms` declared as `application/xml` (which
|
|
149
|
+
* is in the default type allowlist, and which a ClickOnce manifest genuinely
|
|
150
|
+
* is) therefore reached the download directory under its own name with no note
|
|
151
|
+
* against it. The property test over the whole set is what keeps the two in
|
|
152
|
+
* step from here on.
|
|
153
|
+
*/
|
|
136
154
|
export function extensionOf(filename) {
|
|
137
|
-
const match = /\.([A-Za-z0-9]{1,
|
|
155
|
+
const match = /\.([A-Za-z0-9-]{1,16})$/.exec(filename);
|
|
138
156
|
return match?.[1]?.toLowerCase() ?? '';
|
|
139
157
|
}
|
|
140
158
|
/**
|