@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.
Files changed (67) hide show
  1. package/README.md +142 -35
  2. package/dist/analyze.d.ts +54 -6
  3. package/dist/analyze.js +295 -27
  4. package/dist/analyze.js.map +1 -1
  5. package/dist/attachments.d.ts +36 -0
  6. package/dist/attachments.js +21 -3
  7. package/dist/attachments.js.map +1 -1
  8. package/dist/audit.d.ts +7 -0
  9. package/dist/audit.js +11 -3
  10. package/dist/audit.js.map +1 -1
  11. package/dist/config.d.ts +32 -0
  12. package/dist/config.js +65 -6
  13. package/dist/config.js.map +1 -1
  14. package/dist/errors.js.map +1 -1
  15. package/dist/extract/child.d.ts +1 -0
  16. package/dist/extract/child.js +83 -0
  17. package/dist/extract/child.js.map +1 -0
  18. package/dist/extract/index.d.ts +41 -0
  19. package/dist/extract/index.js +183 -0
  20. package/dist/extract/index.js.map +1 -0
  21. package/dist/extract/ooxml.d.ts +35 -0
  22. package/dist/extract/ooxml.js +627 -0
  23. package/dist/extract/ooxml.js.map +1 -0
  24. package/dist/extract/pdf.d.ts +62 -0
  25. package/dist/extract/pdf.js +539 -0
  26. package/dist/extract/pdf.js.map +1 -0
  27. package/dist/extract/types.d.ts +56 -0
  28. package/dist/extract/types.js +13 -0
  29. package/dist/extract/types.js.map +1 -0
  30. package/dist/imap.js.map +1 -1
  31. package/dist/index.js +32 -5
  32. package/dist/index.js.map +1 -1
  33. package/dist/output-schema.d.ts +63 -0
  34. package/dist/output-schema.js +87 -0
  35. package/dist/output-schema.js.map +1 -0
  36. package/dist/resources.d.ts +1 -1
  37. package/dist/resources.js +5 -2
  38. package/dist/resources.js.map +1 -1
  39. package/dist/result.d.ts +39 -6
  40. package/dist/result.js +142 -29
  41. package/dist/result.js.map +1 -1
  42. package/dist/schema.d.ts +13 -1
  43. package/dist/schema.js +20 -2
  44. package/dist/schema.js.map +1 -1
  45. package/dist/server.d.ts +1 -1
  46. package/dist/server.js +30 -5
  47. package/dist/server.js.map +1 -1
  48. package/dist/tools/annotations.d.ts +32 -0
  49. package/dist/tools/annotations.js +33 -0
  50. package/dist/tools/annotations.js.map +1 -0
  51. package/dist/tools/catalogue.d.ts +2 -2
  52. package/dist/tools/read.d.ts +1 -1
  53. package/dist/tools/read.js +743 -87
  54. package/dist/tools/read.js.map +1 -1
  55. package/dist/tools/write.d.ts +3 -3
  56. package/dist/tools/write.js +174 -39
  57. package/dist/tools/write.js.map +1 -1
  58. package/package.json +17 -11
  59. package/dist/approval.d.ts +0 -45
  60. package/dist/approval.js +0 -69
  61. package/dist/approval.js.map +0 -1
  62. package/dist/confirm.d.ts +0 -59
  63. package/dist/confirm.js +0 -92
  64. package/dist/confirm.js.map +0 -1
  65. package/dist/tool-filter.d.ts +0 -45
  66. package/dist/tool-filter.js +0 -171
  67. package/dist/tool-filter.js.map +0 -1
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  [![license](https://img.shields.io/npm/l/%40ni-c%2Fimap-mcp)](LICENSE)
8
8
  [![container](https://img.shields.io/badge/ghcr.io-ni--c%2Fimap--mcp-blue)](https://github.com/ni-c/imap-mcp/pkgs/container/imap-mcp)
9
9
  [![docs](https://img.shields.io/badge/docs-imap--mcp.ni--c.de-informational)](https://imap-mcp.ni-c.de)
10
+ [![HTTP • via mcp-hub](https://img.shields.io/badge/HTTP-via%20mcp--hub-6f42c1)](https://mcp-hub.ni-c.de)
10
11
  [![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](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 asks a person.** Where the client supports MCP elicitation, `delete_messages` and
83
- deleting a folder raise a real dialog that the model cannot answer on its behalf. Where it does
84
- not, they fall back to a two-call token — and say so, rather than implying somebody approved.
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
- | `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_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 two size limits are
150
- separate on purpose: one protects the model's context window, the other protects your disk.
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` | 🔒 for both `move` and `copy` |
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. The first is the whole security argument (see `SECURITY.md`); the
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. Best effort, not a guarantee: nested same-name tags
32
- * end the non-greedy match early, and elements hidden via a stylesheet class
33
- * are not recognised at all.
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
- /** Names of the injection shapes present in `text`. */
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.