@ni-c/imap-mcp 0.2.0 → 0.4.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 +142 -35
- package/dist/analyze.d.ts +54 -6
- package/dist/analyze.js +295 -27
- package/dist/analyze.js.map +1 -1
- package/dist/attachments.d.ts +36 -0
- package/dist/attachments.js +21 -3
- 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 +32 -0
- package/dist/config.js +65 -6
- package/dist/config.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/extract/child.d.ts +1 -0
- package/dist/extract/child.js +83 -0
- package/dist/extract/child.js.map +1 -0
- package/dist/extract/index.d.ts +41 -0
- package/dist/extract/index.js +183 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/ooxml.d.ts +35 -0
- package/dist/extract/ooxml.js +627 -0
- package/dist/extract/ooxml.js.map +1 -0
- package/dist/extract/pdf.d.ts +62 -0
- package/dist/extract/pdf.js +539 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/types.d.ts +56 -0
- package/dist/extract/types.js +13 -0
- package/dist/extract/types.js.map +1 -0
- 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 +63 -0
- package/dist/output-schema.js +87 -0
- package/dist/output-schema.js.map +1 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +5 -2
- package/dist/resources.js.map +1 -1
- package/dist/result.d.ts +39 -6
- package/dist/result.js +142 -29
- 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 +743 -87
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +3 -3
- package/dist/tools/write.js +174 -39
- package/dist/tools/write.js.map +1 -1
- package/package.json +17 -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,28 @@ 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
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
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_MAX_EXTRACT_BYTES` | no | `10485760` | Ceiling for reading a document's text; max `67108864` |
|
|
120
|
+
| `IMAP_ATTACHMENT_TYPES` | no | see below | Comma-separated content-type allowlist |
|
|
121
|
+
| `IMAP_DOWNLOAD_DIR` | no | — | Setting it allows saving attachments there |
|
|
122
|
+
| `IMAP_INSECURE_TLS` | no | `false` | Exactly `true` accepts a self-signed certificate |
|
|
123
|
+
| `ELICITATION` | no | `true` | `false` replaces the dialog with the token. **Not prefixed** |
|
|
114
124
|
|
|
115
125
|
Booleans are compared against the literal string `true`; `1`, `yes` and `True` are not true.
|
|
116
126
|
`IMAP_READ_ONLY` is the mirror image: only the literal `false` turns it off, so a typo leaves
|
|
@@ -146,8 +156,11 @@ answer — its `/hub` endpoint replaces every server's tools with six meta-tools
|
|
|
146
156
|
The password is deleted from the process environment as soon as it is read, so it is not
|
|
147
157
|
visible to child processes or in `/proc/<pid>/environ`.
|
|
148
158
|
|
|
149
|
-
Without `IMAP_DOWNLOAD_DIR` this server never writes to the filesystem. The
|
|
150
|
-
separate on purpose
|
|
159
|
+
Without `IMAP_DOWNLOAD_DIR` this server never writes to the filesystem. The three size limits are
|
|
160
|
+
separate on purpose, because they answer three different questions:
|
|
161
|
+
`IMAP_MAX_ATTACHMENT_BYTES` protects the model's context window,
|
|
162
|
+
`IMAP_MAX_DOWNLOAD_BYTES` protects your disk, and `IMAP_MAX_EXTRACT_BYTES` bounds how much
|
|
163
|
+
hostile input one parser is handed. Raising any one of them is not a request to raise the others.
|
|
151
164
|
|
|
152
165
|
The server starts without credentials on purpose — it completes the handshake and lists its
|
|
153
166
|
tools, and every call then fails with setup instructions instead of reaching a server.
|
|
@@ -204,6 +217,41 @@ bind mount has to be owned by it on the host: `-e IMAP_DOWNLOAD_DIR=/data -v
|
|
|
204
217
|
"$PWD/attachments:/data"` with `chown 1000:1000 attachments`. Without
|
|
205
218
|
`IMAP_DOWNLOAD_DIR` the container never writes anything.
|
|
206
219
|
|
|
220
|
+
### Through mcp-hub
|
|
221
|
+
|
|
222
|
+
A client that cannot spawn a local process — ChatGPT connectors, Claude on the web,
|
|
223
|
+
Cursor, LibreChat — reaches imap-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one
|
|
224
|
+
container serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login
|
|
225
|
+
behind a single password and long-lived tokens for the clients that cannot do OAuth. Its
|
|
226
|
+
`/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of
|
|
227
|
+
them without N×tool schemas in the model's context, and it speaks both protocol revisions
|
|
228
|
+
— a question this server asks travels through it to the person at the far end.
|
|
229
|
+
|
|
230
|
+
Its `/config/mcp.json` uses Claude Code's format, so the entry is the one you already
|
|
231
|
+
have:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"mcpServers": {
|
|
236
|
+
"imap-mcp": {
|
|
237
|
+
"command": "npx",
|
|
238
|
+
"args": ["-y", "@ni-c/imap-mcp"],
|
|
239
|
+
"env": {
|
|
240
|
+
"IMAP_HOST": "imap.example.net",
|
|
241
|
+
"IMAP_USER": "me@example.net",
|
|
242
|
+
"IMAP_PASSWORD": "…",
|
|
243
|
+
"IMAP_ALLOW_TOOLS": "essential"
|
|
244
|
+
},
|
|
245
|
+
"denyTools": ["delete_messages"]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not
|
|
252
|
+
the same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,
|
|
253
|
+
are in the [client guide](https://imap-mcp.ni-c.de/guide/clients#through-mcp-hub).
|
|
254
|
+
|
|
207
255
|
## Tools
|
|
208
256
|
|
|
209
257
|
**Read** — always registered
|
|
@@ -215,17 +263,17 @@ bind mount has to be owned by it on the host: `-e IMAP_DOWNLOAD_DIR=/data -v
|
|
|
215
263
|
| `list_messages` | Lists and searches: sender, recipient, subject, body, date range, flags |
|
|
216
264
|
| `list_new_messages` | Messages not handed over yet; marks them afterwards, `dry_run` to preview |
|
|
217
265
|
| `get_message` | Headers and body, fenced untrusted, plus the security assessment; `include_thread` |
|
|
218
|
-
| `get_attachments` | Without `part_id` lists them, with `part_id` reads or saves one
|
|
266
|
+
| `get_attachments` | Without `part_id` lists them, with `part_id` reads, extracts or saves one |
|
|
219
267
|
|
|
220
268
|
**Mailbox** — needs `IMAP_READ_ONLY=false`
|
|
221
269
|
|
|
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
|
|
270
|
+
| Tool | Confirmation |
|
|
271
|
+
| ------------------- | --------------------------------------------------------- |
|
|
272
|
+
| `set_message_flags` | none — flags are reversible, and `\Deleted` is refused |
|
|
273
|
+
| `move_messages` | 👤 for both `move` and `copy`, 🔒 where the client cannot |
|
|
274
|
+
| `delete_messages` | 👤 asks the user, 🔒 where the client cannot |
|
|
275
|
+
| `manage_mailbox` | 👤 for `delete`, 🔒 for `rename`, none for `create` |
|
|
276
|
+
| `save_draft` | none — a draft does not leave the mailbox |
|
|
229
277
|
|
|
230
278
|
👤 raises a dialog the model cannot answer · 🔒 needs a confirmation token: call once to
|
|
231
279
|
receive one, then again with it.
|
|
@@ -240,6 +288,41 @@ it. Use `delete_messages`, which asks.
|
|
|
240
288
|
Neither a confirmation nor a dialog quotes a mailbox name inside its own sentence — folder
|
|
241
289
|
names come from the account, which on a shared mailbox means a colleague chose them.
|
|
242
290
|
|
|
291
|
+
### Structured output
|
|
292
|
+
|
|
293
|
+
Every tool declares an `outputSchema` and answers with `structuredContent`
|
|
294
|
+
alongside the text block, so a client can use the result without parsing prose:
|
|
295
|
+
|
|
296
|
+
```jsonc
|
|
297
|
+
{
|
|
298
|
+
"untrusted": true,
|
|
299
|
+
"source": "imap",
|
|
300
|
+
"mailbox": "INBOX",
|
|
301
|
+
"total_matching": 214,
|
|
302
|
+
"offset": 0,
|
|
303
|
+
"returned": 25,
|
|
304
|
+
"next_offset": 25,
|
|
305
|
+
"messages": [{ "uid": 4711, "subject": "…", "from": "…", "seen": false }],
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Every tool that reports anything out of the mailbox carries `untrusted: true`
|
|
310
|
+
and `source: "imap"` as fields — a sender display name, a folder name a
|
|
311
|
+
colleague chose and an attachment filename are all attacker-controllable, and
|
|
312
|
+
they reach the model through the listing tools long before anyone opens a
|
|
313
|
+
message. Only `get_server_info` and the five write tools are without it: those
|
|
314
|
+
report this server's own configuration, or what it just did with the uids it was
|
|
315
|
+
given.
|
|
316
|
+
|
|
317
|
+
`get_message` and a text attachment keep the per-call nonce fence in the text
|
|
318
|
+
block — the structured half states the same fields, so a client is not made to
|
|
319
|
+
parse the fence to find them. An image attachment keeps its bytes in the content
|
|
320
|
+
block, where a client renders them, rather than repeating the base64.
|
|
321
|
+
|
|
322
|
+
A refusal is now an **error result**: an attachment the policy rejects, one whose
|
|
323
|
+
bytes are an executable whatever it claimed, one too large to inline. Each was a
|
|
324
|
+
plain result that read like an answer.
|
|
325
|
+
|
|
243
326
|
Attachments are also available as MCP resources at `imap://message/{uid}/part/{partId}`, which
|
|
244
327
|
matters where the server has no useful filesystem. The resource path runs the same allowlist,
|
|
245
328
|
size limit and magic-byte check as the tool — it is not a second, unguarded door.
|
|
@@ -247,7 +330,8 @@ size limit and magic-byte check as the tool — it is not a second, unguarded do
|
|
|
247
330
|
## Not exposed, on purpose
|
|
248
331
|
|
|
249
332
|
No sending, no SMTP, no raw IMAP passthrough, no `APPEND` of arbitrary MIME, no HTML
|
|
250
|
-
composition, no OAuth2
|
|
333
|
+
composition, no OAuth2, and **no OCR** — a scanned PDF has no text to extract and says so
|
|
334
|
+
rather than guessing. The first is the whole security argument (see `SECURITY.md`); the
|
|
251
335
|
second would make every guard here optional; the last is planned but needs a test account
|
|
252
336
|
before it ships.
|
|
253
337
|
|
|
@@ -267,6 +351,17 @@ knowing before concluding that a filtered install reaches less of the mailbox th
|
|
|
267
351
|
saving to disk, where it would be more dangerous, not less.
|
|
268
352
|
- **A `part_id` must come from a listing call**, so the body cannot be pulled out through the
|
|
269
353
|
attachment tool and escape its framing.
|
|
354
|
+
- **Documents are parsed in a process that can be killed.** `mode: "text"` reads a PDF or
|
|
355
|
+
Office file with a bundled PDF.js and a ZIP reader — the only place this server parses a
|
|
356
|
+
binary a stranger sent. It runs in a child process with a heap limit and a timeout, its
|
|
357
|
+
stdout discarded rather than shared with the JSON-RPC transport, PDF.js's `eval` support
|
|
358
|
+
off, compressed streams measured against a ceiling _before_ PDF.js inflates them, and an
|
|
359
|
+
entry allowlist that decides what is decompressed _before_ the buffer is sized. Nothing in
|
|
360
|
+
that path touches the network or the filesystem.
|
|
361
|
+
- **Extracted text says what it is.** Extraction returns every text-drawing instruction in a
|
|
362
|
+
file, including text set below one point or drawn in the colour of the paper, and returns
|
|
363
|
+
nothing that was drawn as a picture. The result says so above the fence, because "the
|
|
364
|
+
document says X" is otherwise a claim the user has no way to check.
|
|
270
365
|
- **Downloads cannot escape their directory.** The target comes only from the environment, the
|
|
271
366
|
filename is sanitised, the resolved path is re-checked, and the file is opened with `wx` and
|
|
272
367
|
mode `0600` — so nothing is overwritten and no planted symlink is followed.
|
|
@@ -282,6 +377,11 @@ knowing before concluding that a filtered install reaches less of the mailbox th
|
|
|
282
377
|
`SECURITY.md` has the trust model, what these measures do _not_ cover, and how to report a
|
|
283
378
|
vulnerability.
|
|
284
379
|
|
|
380
|
+
## Documentation
|
|
381
|
+
|
|
382
|
+
The full guide, tool reference and security notes live at
|
|
383
|
+
**[imap-mcp.ni-c.de](https://imap-mcp.ni-c.de)** (source in [`docs/`](docs/)).
|
|
384
|
+
|
|
285
385
|
## Development
|
|
286
386
|
|
|
287
387
|
```bash
|
|
@@ -303,6 +403,13 @@ network. For a live server to point the real thing at, see
|
|
|
303
403
|
The release workflow publishes to npm (Trusted Publishing, with provenance), creates
|
|
304
404
|
the GitHub release from the CHANGELOG section and updates the MCP Registry entry.
|
|
305
405
|
|
|
406
|
+
## Contributing
|
|
407
|
+
|
|
408
|
+
Issues, discussions and pull requests are welcome — see
|
|
409
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use
|
|
410
|
+
[private reporting](https://github.com/ni-c/imap-mcp/security/advisories/new)
|
|
411
|
+
rather than a public issue; the policy is in [SECURITY.md](SECURITY.md).
|
|
412
|
+
|
|
306
413
|
## License
|
|
307
414
|
|
|
308
|
-
MIT © Willi Thiel
|
|
415
|
+
[MIT](LICENSE) © Willi Thiel
|
package/dist/analyze.d.ts
CHANGED
|
@@ -23,16 +23,44 @@ export interface SecurityAssessment {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Extracts readable text from HTML
|
|
26
|
+
* Extracts readable text from HTML — and from the XML inside an OOXML or
|
|
27
|
+
* OpenDocument attachment, which is the same problem with different tag names.
|
|
28
|
+
*
|
|
29
|
+
* Reused there rather than reimplemented, and that is a security decision, not
|
|
30
|
+
* a tidiness one. The obvious `document.xml` reader is
|
|
31
|
+
* `/<w:t[^>]*>([\s\S]*?)<\/w:t>/g` — which is the exact shape of the bug
|
|
32
|
+
* recorded above this function: *n* start tokens each paying for a scan that
|
|
33
|
+
* never finds its end. A `document.xml` of `'<w:t '` repeated 200 000 times is
|
|
34
|
+
* a tiny, deflate-friendly ZIP entry. This walk is immune for the reason it was
|
|
35
|
+
* written, so the second parser is the one not to write.
|
|
36
|
+
*
|
|
37
|
+
* It is also why nothing here parses XML properly. A real parser resolves
|
|
38
|
+
* entities, and would hand a mail attachment billion-laughs expansion and an
|
|
39
|
+
* `<!ENTITY … SYSTEM "file:///etc/passwd">` that reads a file. Those are not
|
|
40
|
+
* defended against below; they are simply not implemented. `&lol9;` comes out
|
|
41
|
+
* as six literal characters, and it must stay that way — reaching for
|
|
42
|
+
* `fast-xml-parser` here would reintroduce all three at once.
|
|
27
43
|
*
|
|
28
44
|
* Deliberately not `mailparser`'s own `text` fallback: that keeps content the
|
|
29
45
|
* recipient never sees. Anything hidden by inline CSS is a place to park an
|
|
30
46
|
* instruction meant only for the model, so those elements are dropped before
|
|
31
|
-
* the tags are stripped.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
47
|
+
* the tags are stripped.
|
|
48
|
+
*
|
|
49
|
+
* `maxChars` overrides the input slice. The default suits a mail body; a
|
|
50
|
+
* document part needs more, because OOXML spends most of its bytes on
|
|
51
|
+
* formatting and the readable text is a small fraction of it.
|
|
52
|
+
*
|
|
53
|
+
* This is one pass over the input. The cursors below only ever move forward,
|
|
54
|
+
* which is the property that makes the whole function linear no matter what the
|
|
55
|
+
* sender writes: a start token that is never closed is answered once and then
|
|
56
|
+
* never looked for again, instead of restarting a bounded scan at every
|
|
57
|
+
* occurrence. Removal itself stays best effort — nested same-name tags end a
|
|
58
|
+
* block early, elements hidden via a stylesheet class are not recognised at
|
|
59
|
+
* all, and anything past the scan budget is left in place. That is acceptable
|
|
60
|
+
* for the same reason it always was: nothing downstream trusts the stripping,
|
|
61
|
+
* and the fencing in {@link wrapUntrusted} is what carries the weight.
|
|
34
62
|
*/
|
|
35
|
-
export declare function htmlToText(html: string): string;
|
|
63
|
+
export declare function htmlToText(html: string, maxChars?: number): string;
|
|
36
64
|
/**
|
|
37
65
|
* Removes the characters a human reader cannot see but the model can.
|
|
38
66
|
*
|
|
@@ -40,6 +68,17 @@ export declare function htmlToText(html: string): string;
|
|
|
40
68
|
* body, because it is rendered next to one and read with the same eyes.
|
|
41
69
|
*/
|
|
42
70
|
export declare function stripInvisible(input: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The same characters, written out as escapes instead of removed.
|
|
73
|
+
*
|
|
74
|
+
* For the places where a string has to stay recognisable as the exact thing
|
|
75
|
+
* that was asked for — a confirmation dialog, an audit line. Stripping alone
|
|
76
|
+
* says "this is not what it looked like" and then shows something that looks
|
|
77
|
+
* like an ordinary name; this shows which characters were in it, so a person
|
|
78
|
+
* deciding whether to move mail into `Archive<U+202E>` can see that the folder is
|
|
79
|
+
* not the `Archive` they know.
|
|
80
|
+
*/
|
|
81
|
+
export declare function escapeInvisible(input: string): string;
|
|
43
82
|
/**
|
|
44
83
|
* Normalises text before it reaches the model: Unicode-folded, stripped of the
|
|
45
84
|
* characters a human reader cannot see, auto-fetch markup defused, and
|
|
@@ -55,7 +94,16 @@ export declare function stripInvisible(input: string): string;
|
|
|
55
94
|
* normalising would miss it.
|
|
56
95
|
*/
|
|
57
96
|
export declare function sanitizeText(input: string, maxChars?: number): string;
|
|
58
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Names of the injection shapes present in `text`.
|
|
99
|
+
*
|
|
100
|
+
* Runs in this process on text the sender wrote, up to a million characters
|
|
101
|
+
* of it for an extracted document — after the parser child has exited, so no
|
|
102
|
+
* timeout covers it. Every pattern above therefore has to be linear on a
|
|
103
|
+
* hostile repetition of its own trigger, and `analyze.test.ts` times each one
|
|
104
|
+
* on such input. A new pattern gets a line in that test before it gets a line
|
|
105
|
+
* in the list.
|
|
106
|
+
*/
|
|
59
107
|
export declare function detectSuspicious(text: string): string[];
|
|
60
108
|
/**
|
|
61
109
|
* Words that mix Latin with Cyrillic or Greek letters.
|