@ni-c/imap-mcp 0.2.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/analyze.d.ts +129 -0
  4. package/dist/analyze.js +313 -0
  5. package/dist/analyze.js.map +1 -0
  6. package/dist/approval.d.ts +45 -0
  7. package/dist/approval.js +69 -0
  8. package/dist/approval.js.map +1 -0
  9. package/dist/attachments.d.ts +55 -0
  10. package/dist/attachments.js +270 -0
  11. package/dist/attachments.js.map +1 -0
  12. package/dist/audit.d.ts +17 -0
  13. package/dist/audit.js +33 -0
  14. package/dist/audit.js.map +1 -0
  15. package/dist/config.d.ts +75 -0
  16. package/dist/config.js +202 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/confirm.d.ts +59 -0
  19. package/dist/confirm.js +92 -0
  20. package/dist/confirm.js.map +1 -0
  21. package/dist/download.d.ts +23 -0
  22. package/dist/download.js +65 -0
  23. package/dist/download.js.map +1 -0
  24. package/dist/draft.d.ts +34 -0
  25. package/dist/draft.js +119 -0
  26. package/dist/draft.js.map +1 -0
  27. package/dist/errors.d.ts +15 -0
  28. package/dist/errors.js +24 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/imap.d.ts +161 -0
  31. package/dist/imap.js +300 -0
  32. package/dist/imap.js.map +1 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +36 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/message.d.ts +51 -0
  37. package/dist/message.js +155 -0
  38. package/dist/message.js.map +1 -0
  39. package/dist/resources.d.ts +16 -0
  40. package/dist/resources.js +89 -0
  41. package/dist/resources.js.map +1 -0
  42. package/dist/result.d.ts +57 -0
  43. package/dist/result.js +193 -0
  44. package/dist/result.js.map +1 -0
  45. package/dist/schema.d.ts +42 -0
  46. package/dist/schema.js +99 -0
  47. package/dist/schema.js.map +1 -0
  48. package/dist/server.d.ts +8 -0
  49. package/dist/server.js +64 -0
  50. package/dist/server.js.map +1 -0
  51. package/dist/stream.d.ts +9 -0
  52. package/dist/stream.js +25 -0
  53. package/dist/stream.js.map +1 -0
  54. package/dist/tool-filter.d.ts +45 -0
  55. package/dist/tool-filter.js +171 -0
  56. package/dist/tool-filter.js.map +1 -0
  57. package/dist/tools/catalogue.d.ts +46 -0
  58. package/dist/tools/catalogue.js +67 -0
  59. package/dist/tools/catalogue.js.map +1 -0
  60. package/dist/tools/read.d.ts +4 -0
  61. package/dist/tools/read.js +576 -0
  62. package/dist/tools/read.js.map +1 -0
  63. package/dist/tools/write.d.ts +5 -0
  64. package/dist/tools/write.js +291 -0
  65. package/dist/tools/write.js.map +1 -0
  66. package/package.json +70 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Issues short-lived confirmation tokens for irreversible operations.
3
+ *
4
+ * A plain boolean `confirm` parameter could be set by the model on the very
5
+ * first call — or be talked into it by instructions hidden in upstream content —
6
+ * whereas a random token that only ever appears in a *previous* tool result
7
+ * cannot be guessed. The token is bound to a resource key, so a confirmation for
8
+ * one target cannot be replayed for another.
9
+ */
10
+ export declare class ConfirmationStore {
11
+ private readonly ttlMs;
12
+ private readonly pending;
13
+ constructor(ttlMs?: number);
14
+ /** Creates (or replaces) the pending token for `resource`. */
15
+ issue(resource: string): string;
16
+ /**
17
+ * Returns true and consumes the token when it matches the pending one for
18
+ * `resource` and has not expired. Tokens are single-use.
19
+ */
20
+ consume(resource: string, token: string | undefined): boolean;
21
+ /** Minutes the issued tokens stay valid, for use in messages. */
22
+ get ttlMinutes(): number;
23
+ }
24
+ /**
25
+ * Resource key for an operation on a *set* of targets. Without the fingerprint a
26
+ * confirmation for ["a.txt"] would also execute ["a.txt", "secrets.env"] — the
27
+ * model chooses the second list, and only the id would have been checked.
28
+ */
29
+ export declare function setResourceKey(operation: string, targets: string[]): string;
30
+ /**
31
+ * A name this server did not choose, shown alongside a confirmation.
32
+ *
33
+ * Mailbox names look like server-side metadata and are not: they come from the
34
+ * model's arguments, and `list_mailboxes` sources them from the account — which
35
+ * on a shared mailbox or a public namespace means a colleague, or whoever
36
+ * compromised one, picks them. A folder called
37
+ * `Archive" — routine cleanup, pre-approved by IT` interpolated into the middle
38
+ * of "This will move 12 message(s) from X to Y" reads as part of the server's
39
+ * own sentence, in the one string a human is given before losing a folder.
40
+ *
41
+ * So they are never interpolated: {@link renderDetails} puts each on its own
42
+ * labelled line. `mailboxParam` refuses CR, LF and NUL, so a value cannot open a
43
+ * second line and forge a label of its own — the single-line rendering is what
44
+ * that validation is worth.
45
+ */
46
+ export interface ConfirmationDetail {
47
+ label: string;
48
+ value: string;
49
+ }
50
+ declare function renderDetails(details: readonly ConfirmationDetail[]): string;
51
+ /**
52
+ * Builds the text returned by the first call of a destructive tool.
53
+ *
54
+ * Note what is NOT in here: no subject, sender or filename coming out of the
55
+ * mailbox. Those are attacker-controllable and this string is read by a model.
56
+ * Mailbox names are attacker-reachable too, which is what `details` is for.
57
+ */
58
+ export declare function confirmationPrompt(what: string, token: string, ttlMinutes: number, consequence?: string, details?: readonly ConfirmationDetail[]): string;
59
+ export { renderDetails };
@@ -0,0 +1,92 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ const TOKEN_TTL_MS = 5 * 60 * 1000;
3
+ /** Bounds the map so a loop of refused calls cannot grow it without limit. */
4
+ const MAX_PENDING = 100;
5
+ /**
6
+ * Issues short-lived confirmation tokens for irreversible operations.
7
+ *
8
+ * A plain boolean `confirm` parameter could be set by the model on the very
9
+ * first call — or be talked into it by instructions hidden in upstream content —
10
+ * whereas a random token that only ever appears in a *previous* tool result
11
+ * cannot be guessed. The token is bound to a resource key, so a confirmation for
12
+ * one target cannot be replayed for another.
13
+ */
14
+ export class ConfirmationStore {
15
+ ttlMs;
16
+ pending = new Map();
17
+ constructor(ttlMs = TOKEN_TTL_MS) {
18
+ this.ttlMs = ttlMs;
19
+ }
20
+ /** Creates (or replaces) the pending token for `resource`. */
21
+ issue(resource) {
22
+ if (this.pending.size >= MAX_PENDING) {
23
+ const oldest = this.pending.keys().next();
24
+ if (!oldest.done)
25
+ this.pending.delete(oldest.value);
26
+ }
27
+ const token = randomBytes(16).toString('hex');
28
+ this.pending.set(resource, { token, expiresAt: Date.now() + this.ttlMs });
29
+ return token;
30
+ }
31
+ /**
32
+ * Returns true and consumes the token when it matches the pending one for
33
+ * `resource` and has not expired. Tokens are single-use.
34
+ */
35
+ consume(resource, token) {
36
+ const entry = this.pending.get(resource);
37
+ if (entry === undefined || token === undefined)
38
+ return false;
39
+ const supplied = Buffer.from(token);
40
+ const expected = Buffer.from(entry.token);
41
+ // Constant-time comparison. Guessing 128 random bits through a timing
42
+ // side channel is not a realistic attack on a local tool, but the safe
43
+ // comparison costs one line and removes the question.
44
+ const matches = supplied.length === expected.length &&
45
+ timingSafeEqual(supplied, expected);
46
+ if (!matches)
47
+ return false;
48
+ // Delete on any match, expired or not. Leaving a matched-but-dead entry
49
+ // behind kept it competing for space with live ones, and the eviction in
50
+ // issue() is insertion-order rather than LRU, so a long-lived key could be
51
+ // dropped ahead of a token that was already spent.
52
+ this.pending.delete(resource);
53
+ return Date.now() < entry.expiresAt;
54
+ }
55
+ /** Minutes the issued tokens stay valid, for use in messages. */
56
+ get ttlMinutes() {
57
+ return Math.round(this.ttlMs / 60_000);
58
+ }
59
+ }
60
+ /**
61
+ * Resource key for an operation on a *set* of targets. Without the fingerprint a
62
+ * confirmation for ["a.txt"] would also execute ["a.txt", "secrets.env"] — the
63
+ * model chooses the second list, and only the id would have been checked.
64
+ */
65
+ export function setResourceKey(operation, targets) {
66
+ const fingerprint = createHash('sha256')
67
+ .update(JSON.stringify([...targets].sort()))
68
+ .digest('hex')
69
+ .slice(0, 16);
70
+ return `${operation}:${fingerprint}`;
71
+ }
72
+ function renderDetails(details) {
73
+ if (details.length === 0)
74
+ return '';
75
+ return ('\n\nNames below are supplied by the caller, not by this server:\n' +
76
+ details.map((d) => ` ${d.label}: ${d.value}`).join('\n'));
77
+ }
78
+ /**
79
+ * Builds the text returned by the first call of a destructive tool.
80
+ *
81
+ * Note what is NOT in here: no subject, sender or filename coming out of the
82
+ * mailbox. Those are attacker-controllable and this string is read by a model.
83
+ * Mailbox names are attacker-reachable too, which is what `details` is for.
84
+ */
85
+ export function confirmationPrompt(what, token, ttlMinutes, consequence = 'The operation is irreversible.', details = []) {
86
+ return (`This will ${what}. ${consequence}` +
87
+ `${renderDetails(details)}\n\n` +
88
+ `To proceed, call this tool again with confirm_token="${token}".\n` +
89
+ `The token is valid for ${ttlMinutes} minutes and can be used once.`);
90
+ }
91
+ export { renderDetails };
92
+ //# sourceMappingURL=confirm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirm.js","sourceRoot":"","sources":["../src/confirm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ export interface SavedAttachment {
2
+ path: string;
3
+ bytes: number;
4
+ }
5
+ /**
6
+ * Writes an attachment into the configured download directory.
7
+ *
8
+ * Everything here exists because this is the first point in the server where
9
+ * bytes from a stranger get a name in the filesystem:
10
+ *
11
+ * - The directory comes only from `IMAP_DOWNLOAD_DIR`. A caller — and therefore
12
+ * a message that talked the model into a tool call — cannot choose where
13
+ * anything lands.
14
+ * - The filename is already stripped of path separators and directional
15
+ * overrides by `sanitizeFilename`, but the resolved path is checked against
16
+ * the directory anyway, because one guard is not a guard.
17
+ * - `wx` refuses to open an existing path. That covers two attacks at once:
18
+ * overwriting a file the user cares about, and following a symlink somebody
19
+ * planted under a predictable attachment name.
20
+ * - Mode 0600, because the content is untrusted and possibly confidential at
21
+ * the same time.
22
+ */
23
+ export declare function saveAttachment(directory: string, filename: string, content: Buffer): Promise<SavedAttachment>;
@@ -0,0 +1,65 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { extname, join, resolve, sep } from 'node:path';
3
+ import { ToolInputError } from './errors.js';
4
+ /** How many `name (2).pdf`, `name (3).pdf` … variants are tried. */
5
+ const MAX_COLLISION_ATTEMPTS = 50;
6
+ /**
7
+ * Writes an attachment into the configured download directory.
8
+ *
9
+ * Everything here exists because this is the first point in the server where
10
+ * bytes from a stranger get a name in the filesystem:
11
+ *
12
+ * - The directory comes only from `IMAP_DOWNLOAD_DIR`. A caller — and therefore
13
+ * a message that talked the model into a tool call — cannot choose where
14
+ * anything lands.
15
+ * - The filename is already stripped of path separators and directional
16
+ * overrides by `sanitizeFilename`, but the resolved path is checked against
17
+ * the directory anyway, because one guard is not a guard.
18
+ * - `wx` refuses to open an existing path. That covers two attacks at once:
19
+ * overwriting a file the user cares about, and following a symlink somebody
20
+ * planted under a predictable attachment name.
21
+ * - Mode 0600, because the content is untrusted and possibly confidential at
22
+ * the same time.
23
+ */
24
+ export async function saveAttachment(directory, filename, content) {
25
+ const base = resolve(directory);
26
+ const extension = extname(filename);
27
+ const stem = extension === '' ? filename : filename.slice(0, -extension.length);
28
+ for (let attempt = 1; attempt <= MAX_COLLISION_ATTEMPTS; attempt += 1) {
29
+ const candidate = attempt === 1 ? filename : `${stem} (${attempt})${extension}`;
30
+ const target = resolve(join(base, candidate));
31
+ // The sanitizer should already have made this impossible; if it ever stops
32
+ // being true, the write must not be the place where that is discovered.
33
+ if (target !== base && !target.startsWith(base + sep)) {
34
+ throw new ToolInputError('imap-mcp: refused to write outside the configured download directory.');
35
+ }
36
+ let handle;
37
+ try {
38
+ handle = await open(target, 'wx', 0o600);
39
+ }
40
+ catch (error) {
41
+ if (error.code === 'EEXIST')
42
+ continue;
43
+ throw new ToolInputError(`imap-mcp: could not write to the download directory: ${describe(error)}`);
44
+ }
45
+ try {
46
+ await handle.write(content);
47
+ return { path: target, bytes: content.length };
48
+ }
49
+ finally {
50
+ await handle.close();
51
+ }
52
+ }
53
+ throw new ToolInputError(`imap-mcp: ${MAX_COLLISION_ATTEMPTS} files with this name already exist in the download directory.`);
54
+ }
55
+ function describe(error) {
56
+ const code = error?.code;
57
+ if (code === 'ENOENT')
58
+ return 'the directory does not exist';
59
+ if (code === 'EACCES' || code === 'EPERM')
60
+ return 'permission denied';
61
+ if (code === 'ENOSPC')
62
+ return 'no space left on the device';
63
+ return code ?? 'unknown error';
64
+ }
65
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,oEAAoE;AACpE,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAOlC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,SAAiB,EACjB,QAAgB,EAChB,OAAe;IAEf,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,IAAI,GACR,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAErE,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,sBAAsB,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACtE,MAAM,SAAS,GACb,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,IAAI,SAAS,EAAE,CAAC;QAChE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;QAE9C,2EAA2E;QAC3E,wEAAwE;QACxE,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,cAAc,CACtB,uEAAuE,CACxE,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,SAAS;YACjE,MAAM,IAAI,cAAc,CACtB,wDAAwD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAC1E,CAAC;QACJ,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;QACjD,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,cAAc,CACtB,aAAa,sBAAsB,gEAAgE,CACpG,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,MAAM,IAAI,GAAI,KAAsC,EAAE,IAAI,CAAC;IAC3D,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,8BAA8B,CAAC;IAC7D,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,mBAAmB,CAAC;IACtE,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,6BAA6B,CAAC;IAC5D,OAAO,IAAI,IAAI,eAAe,CAAC;AACjC,CAAC"}
@@ -0,0 +1,34 @@
1
+ /** Headers linking a draft into an existing conversation. */
2
+ export interface ThreadHeaders {
3
+ messageId: string | undefined;
4
+ references: string[];
5
+ }
6
+ export interface DraftInput {
7
+ from: string | undefined;
8
+ to: string[];
9
+ cc?: string[];
10
+ bcc?: string[];
11
+ subject: string;
12
+ body: string;
13
+ thread?: ThreadHeaders;
14
+ /** Injected by the tests; real calls stamp the current time. */
15
+ date?: Date;
16
+ }
17
+ /**
18
+ * Builds an RFC 5322 message for `APPEND`.
19
+ *
20
+ * Written by hand rather than pulled from a library because the only consumer
21
+ * is this one path, and because every field here needs the same treatment: a
22
+ * bare CR or LF in a header value would let the caller append headers of its
23
+ * own — a Bcc, a Reply-To pointing elsewhere — to a message a human will later
24
+ * send under their own name. The schemas reject line breaks already; this is
25
+ * the second lock on the same door.
26
+ */
27
+ export declare function buildDraft(input: DraftInput): Buffer;
28
+ /**
29
+ * Encodes a header value as MIME encoded-words when it is not plain ASCII.
30
+ *
31
+ * A raw UTF-8 subject is rejected or mangled by a fair number of servers, and
32
+ * the draft has to survive being opened in whatever mail client the person uses.
33
+ */
34
+ export declare function encodeHeaderValue(value: string): string;
package/dist/draft.js ADDED
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ToolInputError } from './errors.js';
3
+ /** Encoded-words must not exceed 75 characters including the delimiters. */
4
+ const ENCODED_WORD_PAYLOAD = 45;
5
+ /**
6
+ * Builds an RFC 5322 message for `APPEND`.
7
+ *
8
+ * Written by hand rather than pulled from a library because the only consumer
9
+ * is this one path, and because every field here needs the same treatment: a
10
+ * bare CR or LF in a header value would let the caller append headers of its
11
+ * own — a Bcc, a Reply-To pointing elsewhere — to a message a human will later
12
+ * send under their own name. The schemas reject line breaks already; this is
13
+ * the second lock on the same door.
14
+ */
15
+ export function buildDraft(input) {
16
+ const from = input.from;
17
+ if (from === undefined || from === '') {
18
+ throw new ToolInputError('imap-mcp: no sender address available — IMAP_USER is not set.');
19
+ }
20
+ const headers = [
21
+ ['From', from],
22
+ ['To', input.to.join(', ')],
23
+ ];
24
+ if (input.cc !== undefined && input.cc.length > 0) {
25
+ headers.push(['Cc', input.cc.join(', ')]);
26
+ }
27
+ if (input.bcc !== undefined && input.bcc.length > 0) {
28
+ headers.push(['Bcc', input.bcc.join(', ')]);
29
+ }
30
+ headers.push(['Subject', encodeHeaderValue(input.subject)]);
31
+ headers.push(['Date', (input.date ?? new Date()).toUTCString()]);
32
+ headers.push(['Message-ID', `<${randomUUID()}@imap-mcp.invalid>`]);
33
+ if (input.thread?.messageId !== undefined) {
34
+ headers.push(['In-Reply-To', input.thread.messageId]);
35
+ }
36
+ if (input.thread !== undefined && input.thread.references.length > 0) {
37
+ headers.push(['References', foldReferences(input.thread.references)]);
38
+ }
39
+ headers.push(['MIME-Version', '1.0']);
40
+ headers.push(['Content-Type', 'text/plain; charset=utf-8']);
41
+ headers.push(['Content-Transfer-Encoding', 'base64']);
42
+ const lines = headers.map(([name, value]) => {
43
+ assertHeaderSafe(name, value);
44
+ return `${name}: ${value}`;
45
+ });
46
+ // base64 for the body: it survives any charset, cannot contain a line that
47
+ // looks like a header, and keeps lines inside the 998-octet limit.
48
+ const encoded = Buffer.from(input.body, 'utf-8')
49
+ .toString('base64')
50
+ .replace(/(.{76})/g, '$1\r\n');
51
+ return Buffer.from(`${lines.join('\r\n')}\r\n\r\n${encoded}\r\n`, 'utf-8');
52
+ }
53
+ /** Keep every folded header line under the RFC 5322 hard limit of 998 octets. */
54
+ const MAX_HEADER_LINE = 900;
55
+ /**
56
+ * Joins the References chain, folding onto continuation lines as needed.
57
+ *
58
+ * Twenty Message-IDs of up to 257 characters each on a single line would pass
59
+ * 5 kB — five times the 998-octet line limit, which some servers answer by
60
+ * refusing the APPEND and others by mangling the header. Folding (CRLF
61
+ * followed by a space) is how RFC 5322 says a long header is written.
62
+ */
63
+ function foldReferences(references) {
64
+ const lines = [];
65
+ let line = '';
66
+ for (const reference of references) {
67
+ if (line === '') {
68
+ line = reference;
69
+ }
70
+ else if (line.length + 1 + reference.length > MAX_HEADER_LINE) {
71
+ lines.push(line);
72
+ line = reference;
73
+ }
74
+ else {
75
+ line = `${line} ${reference}`;
76
+ }
77
+ }
78
+ if (line !== '')
79
+ lines.push(line);
80
+ return lines.join('\r\n ');
81
+ }
82
+ /**
83
+ * A CRLF is only legal in a header when the next line starts with whitespace —
84
+ * that is folding. Anything else ends the header and starts a new one, which is
85
+ * exactly the injection this guards against.
86
+ */
87
+ function assertHeaderSafe(name, value) {
88
+ if (/\r(?!\n)|(?<!\r)\n|\r\n(?![ \t])/.test(value)) {
89
+ throw new ToolInputError(`imap-mcp: the ${name} header must not contain line breaks.`);
90
+ }
91
+ }
92
+ /**
93
+ * Encodes a header value as MIME encoded-words when it is not plain ASCII.
94
+ *
95
+ * A raw UTF-8 subject is rejected or mangled by a fair number of servers, and
96
+ * the draft has to survive being opened in whatever mail client the person uses.
97
+ */
98
+ export function encodeHeaderValue(value) {
99
+ if (!/[^ -~]/.test(value))
100
+ return value;
101
+ const words = [];
102
+ let chunk = '';
103
+ for (const character of value) {
104
+ const candidate = chunk + character;
105
+ if (Buffer.byteLength(candidate, 'utf-8') > ENCODED_WORD_PAYLOAD) {
106
+ words.push(chunk);
107
+ chunk = character;
108
+ }
109
+ else {
110
+ chunk = candidate;
111
+ }
112
+ }
113
+ if (chunk !== '')
114
+ words.push(chunk);
115
+ return words
116
+ .map((word) => `=?UTF-8?B?${Buffer.from(word, 'utf-8').toString('base64')}?=`)
117
+ .join('\r\n ');
118
+ }
119
+ //# sourceMappingURL=draft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /** Errors that come from the caller's arguments rather than from the server. */
2
+ export declare class ToolInputError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * An IMAP failure, carrying whatever the server said.
7
+ *
8
+ * `responseText` is upstream output and gets the same truncation treatment as
9
+ * any other remote string before it reaches the model.
10
+ */
11
+ export declare class MailError extends Error {
12
+ readonly code: string | undefined;
13
+ readonly responseText: string;
14
+ constructor(message: string, code?: string | undefined, responseText?: string);
15
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ /** Errors that come from the caller's arguments rather than from the server. */
2
+ export class ToolInputError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'ToolInputError';
6
+ }
7
+ }
8
+ /**
9
+ * An IMAP failure, carrying whatever the server said.
10
+ *
11
+ * `responseText` is upstream output and gets the same truncation treatment as
12
+ * any other remote string before it reaches the model.
13
+ */
14
+ export class MailError extends Error {
15
+ code;
16
+ responseText;
17
+ constructor(message, code = undefined, responseText = '') {
18
+ super(message);
19
+ this.code = code;
20
+ this.responseText = responseText;
21
+ this.name = 'MailError';
22
+ }
23
+ }
24
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAGvB;IACA;IAHX,YACE,OAAe,EACN,OAA2B,SAAS,EACpC,eAAuB,EAAE;QAElC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,SAAI,GAAJ,IAAI,CAAgC;QACpC,iBAAY,GAAZ,YAAY,CAAa;QAGlC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
package/dist/imap.d.ts ADDED
@@ -0,0 +1,161 @@
1
+ import type { FetchMessageObject, FetchQueryObject, ListResponse, SearchObject, StatusObject } from 'imapflow';
2
+ import { type Config, type ImapConfig } from './config.js';
3
+ import { MailError } from './errors.js';
4
+ /**
5
+ * Everything this server needs from an IMAP connection.
6
+ *
7
+ * Narrowing imapflow to an interface is what makes the tools testable: the unit
8
+ * tests inject a fake through {@link ImapClientFactory} instead of standing up a
9
+ * real server, and the surface stays small enough that the fake cannot silently
10
+ * drift away from the real thing.
11
+ */
12
+ export interface ImapConnection {
13
+ connect(): Promise<void>;
14
+ logout(): Promise<void>;
15
+ close(): void;
16
+ noop(): Promise<void>;
17
+ list(options?: {
18
+ statusQuery?: {
19
+ messages?: boolean;
20
+ unseen?: boolean;
21
+ uidNext?: boolean;
22
+ };
23
+ }): Promise<ListResponse[]>;
24
+ status(path: string, query: Record<string, boolean>): Promise<StatusObject>;
25
+ getMailboxLock(path: string, options?: {
26
+ readOnly?: boolean;
27
+ }): Promise<{
28
+ path: string;
29
+ release(): void;
30
+ }>;
31
+ search(query: SearchObject, options?: {
32
+ uid?: boolean;
33
+ }): Promise<number[] | false>;
34
+ fetch(range: number[] | string, query: FetchQueryObject, options?: {
35
+ uid?: boolean;
36
+ }): AsyncIterableIterator<FetchMessageObject>;
37
+ download(range: string, part?: string, options?: {
38
+ uid?: boolean;
39
+ maxBytes?: number;
40
+ }): Promise<{
41
+ meta: {
42
+ contentType: string;
43
+ charset?: string;
44
+ filename?: string;
45
+ };
46
+ content: NodeJS.ReadableStream;
47
+ }>;
48
+ messageFlagsAdd(range: number[], flags: string[], options?: {
49
+ uid?: boolean;
50
+ }): Promise<boolean>;
51
+ messageFlagsRemove(range: number[], flags: string[], options?: {
52
+ uid?: boolean;
53
+ }): Promise<boolean>;
54
+ messageMove(range: number[], destination: string, options?: {
55
+ uid?: boolean;
56
+ }): Promise<unknown>;
57
+ messageCopy(range: number[], destination: string, options?: {
58
+ uid?: boolean;
59
+ }): Promise<unknown>;
60
+ messageDelete(range: number[], options?: {
61
+ uid?: boolean;
62
+ }): Promise<boolean>;
63
+ append(path: string, content: string | Buffer, flags?: string[], date?: Date): Promise<unknown>;
64
+ mailboxCreate(path: string): Promise<unknown>;
65
+ mailboxRename(path: string, newPath: string): Promise<unknown>;
66
+ mailboxDelete(path: string): Promise<unknown>;
67
+ readonly capabilities: Map<string, boolean | number>;
68
+ readonly mailbox: false | {
69
+ path: string;
70
+ permanentFlags: Set<string>;
71
+ };
72
+ }
73
+ export type ImapClientFactory = (config: ImapConfig) => ImapConnection;
74
+ export interface MailboxSummary {
75
+ path: string;
76
+ name: string;
77
+ delimiter: string;
78
+ specialUse: string | undefined;
79
+ subscribed: boolean;
80
+ selectable: boolean;
81
+ messages: number | undefined;
82
+ unseen: number | undefined;
83
+ uidNext: number | undefined;
84
+ }
85
+ /**
86
+ * Connection manager around a single IMAP account.
87
+ *
88
+ * Reads take the mailbox lock read-only and use BODY.PEEK throughout, so
89
+ * fetching a message never changes what the human sees as unread. The only
90
+ * exception is deliberate and explicit: {@link tagSeen}, which writes the
91
+ * server's own bookkeeping keyword.
92
+ */
93
+ export declare class ImapClient {
94
+ private readonly config;
95
+ private readonly factory;
96
+ private connection;
97
+ private connecting;
98
+ constructor(config: Config, factory?: ImapClientFactory);
99
+ /** Credentials are checked per call, not at startup. */
100
+ private assertConfigured;
101
+ private connection_;
102
+ /**
103
+ * Runs `fn` with the mailbox open and the lock held.
104
+ *
105
+ * A dropped connection is retried exactly once: IMAP sessions are long-lived
106
+ * and idle ones get reaped by servers and NAT gateways alike, so the first
107
+ * call after a pause routinely fails for reasons that have nothing to do with
108
+ * the request.
109
+ */
110
+ withMailbox<T>(mailbox: string | undefined, readOnly: boolean, fn: (client: ImapConnection, path: string) => Promise<T>): Promise<T>;
111
+ private run;
112
+ /** Runs `fn` without selecting a mailbox, for LIST/STATUS/CREATE style calls. */
113
+ withConnection<T>(fn: (client: ImapConnection) => Promise<T>): Promise<T>;
114
+ private forget;
115
+ listMailboxes(): Promise<MailboxSummary[]>;
116
+ /**
117
+ * Whether the account can store the bookkeeping keyword.
118
+ *
119
+ * `\*` in PERMANENTFLAGS means the server accepts arbitrary keywords. Some
120
+ * providers accept none at all, and there the new-mail tracking cannot work —
121
+ * better to say so than to tag silently into the void.
122
+ */
123
+ keywordSupported(permanentFlags: Set<string>): boolean;
124
+ get seenKeyword(): string;
125
+ get defaultMailbox(): string;
126
+ get maxMessages(): number;
127
+ get user(): string | undefined;
128
+ /**
129
+ * Reads the threading headers of one message, for linking a draft into an
130
+ * existing conversation.
131
+ *
132
+ * Only the Message-ID and the References chain are taken; nothing the sender
133
+ * wrote as prose comes back from here.
134
+ */
135
+ threadHeaders(mailbox: string | undefined, uid: number): Promise<{
136
+ messageId: string | undefined;
137
+ references: string[];
138
+ }>;
139
+ search(client: ImapConnection, query: SearchObject): Promise<number[]>;
140
+ /** Envelope-level fetch, newest UID first. */
141
+ fetchSummaries(client: ImapConnection, uids: number[]): Promise<FetchMessageObject[]>;
142
+ /**
143
+ * Adds the bookkeeping keyword to the given UIDs.
144
+ *
145
+ * This is the one write the server performs on its own initiative, and it
146
+ * stays available under the default IMAP_READ_ONLY: without it `list_new_messages`
147
+ * would return the same mail forever. It touches no flag a human interacts
148
+ * with — `\Seen` in particular is left alone.
149
+ */
150
+ tagSeen(client: ImapConnection, uids: number[]): Promise<void>;
151
+ close(): Promise<void>;
152
+ }
153
+ /** Rejects a promise that outlives the command timeout. */
154
+ export declare function withTimeout<T>(promise: Promise<T>, command: string, ms?: number): Promise<T>;
155
+ /**
156
+ * Normalises whatever imapflow threw into a {@link MailError}.
157
+ *
158
+ * Only the response text is carried over, never the whole error object: it
159
+ * holds the command that was sent, and for a LOGIN that means the password.
160
+ */
161
+ export declare function asMailError(error: unknown): MailError;