@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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/analyze.d.ts +129 -0
- package/dist/analyze.js +313 -0
- package/dist/analyze.js.map +1 -0
- package/dist/approval.d.ts +45 -0
- package/dist/approval.js +69 -0
- package/dist/approval.js.map +1 -0
- package/dist/attachments.d.ts +55 -0
- package/dist/attachments.js +270 -0
- package/dist/attachments.js.map +1 -0
- package/dist/audit.d.ts +17 -0
- package/dist/audit.js +33 -0
- package/dist/audit.js.map +1 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +202 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm.d.ts +59 -0
- package/dist/confirm.js +92 -0
- package/dist/confirm.js.map +1 -0
- package/dist/download.d.ts +23 -0
- package/dist/download.js +65 -0
- package/dist/download.js.map +1 -0
- package/dist/draft.d.ts +34 -0
- package/dist/draft.js +119 -0
- package/dist/draft.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/imap.d.ts +161 -0
- package/dist/imap.js +300 -0
- package/dist/imap.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/message.d.ts +51 -0
- package/dist/message.js +155 -0
- package/dist/message.js.map +1 -0
- package/dist/resources.d.ts +16 -0
- package/dist/resources.js +89 -0
- package/dist/resources.js.map +1 -0
- package/dist/result.d.ts +57 -0
- package/dist/result.js +193 -0
- package/dist/result.js.map +1 -0
- package/dist/schema.d.ts +42 -0
- package/dist/schema.js +99 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/stream.d.ts +9 -0
- package/dist/stream.js +25 -0
- package/dist/stream.js.map +1 -0
- package/dist/tool-filter.d.ts +45 -0
- package/dist/tool-filter.js +171 -0
- package/dist/tool-filter.js.map +1 -0
- package/dist/tools/catalogue.d.ts +46 -0
- package/dist/tools/catalogue.js +67 -0
- package/dist/tools/catalogue.js.map +1 -0
- package/dist/tools/read.d.ts +4 -0
- package/dist/tools/read.js +576 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +291 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { defuseAutoFetch, stripInvisible } from './analyze.js';
|
|
2
|
+
/** How deep the MIME tree is walked. Forwarded mail nests; a loop does not. */
|
|
3
|
+
const MAX_DEPTH = 5;
|
|
4
|
+
/** Upper bound on how many parts one listing may describe. */
|
|
5
|
+
const MAX_ATTACHMENTS = 50;
|
|
6
|
+
const MAX_FILENAME_LENGTH = 120;
|
|
7
|
+
/**
|
|
8
|
+
* Extensions that are executable somewhere. The list is long on purpose: this
|
|
9
|
+
* is a refusal list applied on top of the content-type allowlist, and the cost
|
|
10
|
+
* of an extra entry is one unusual attachment that has to be fetched by other
|
|
11
|
+
* means.
|
|
12
|
+
*/
|
|
13
|
+
const EXECUTABLE_EXTENSIONS = new Set([
|
|
14
|
+
'ade',
|
|
15
|
+
'adp',
|
|
16
|
+
'app',
|
|
17
|
+
'appimage',
|
|
18
|
+
'application',
|
|
19
|
+
'appref-ms',
|
|
20
|
+
'asp',
|
|
21
|
+
'aspx',
|
|
22
|
+
'bas',
|
|
23
|
+
'bat',
|
|
24
|
+
'cer',
|
|
25
|
+
'chm',
|
|
26
|
+
'cmd',
|
|
27
|
+
'com',
|
|
28
|
+
'cpl',
|
|
29
|
+
'crt',
|
|
30
|
+
'csh',
|
|
31
|
+
'deb',
|
|
32
|
+
'dll',
|
|
33
|
+
'dmg',
|
|
34
|
+
'exe',
|
|
35
|
+
'fxp',
|
|
36
|
+
'gadget',
|
|
37
|
+
'hlp',
|
|
38
|
+
'hta',
|
|
39
|
+
'inf',
|
|
40
|
+
'ins',
|
|
41
|
+
'iso',
|
|
42
|
+
'isp',
|
|
43
|
+
'its',
|
|
44
|
+
'jar',
|
|
45
|
+
'js',
|
|
46
|
+
'jse',
|
|
47
|
+
'ksh',
|
|
48
|
+
'lnk',
|
|
49
|
+
'mad',
|
|
50
|
+
'maf',
|
|
51
|
+
'mag',
|
|
52
|
+
'mam',
|
|
53
|
+
'maq',
|
|
54
|
+
'mar',
|
|
55
|
+
'mas',
|
|
56
|
+
'mat',
|
|
57
|
+
'mau',
|
|
58
|
+
'mav',
|
|
59
|
+
'maw',
|
|
60
|
+
'mda',
|
|
61
|
+
'mdb',
|
|
62
|
+
'mde',
|
|
63
|
+
'mdt',
|
|
64
|
+
'mdw',
|
|
65
|
+
'mdz',
|
|
66
|
+
'msc',
|
|
67
|
+
'msh',
|
|
68
|
+
'msh1',
|
|
69
|
+
'msh2',
|
|
70
|
+
'mshxml',
|
|
71
|
+
'msi',
|
|
72
|
+
'msp',
|
|
73
|
+
'mst',
|
|
74
|
+
'ops',
|
|
75
|
+
'pcd',
|
|
76
|
+
'pif',
|
|
77
|
+
'pkg',
|
|
78
|
+
'pl',
|
|
79
|
+
'plg',
|
|
80
|
+
'prf',
|
|
81
|
+
'prg',
|
|
82
|
+
'ps1',
|
|
83
|
+
'ps2',
|
|
84
|
+
'psc1',
|
|
85
|
+
'psc2',
|
|
86
|
+
'py',
|
|
87
|
+
'pyc',
|
|
88
|
+
'pyo',
|
|
89
|
+
'rb',
|
|
90
|
+
'reg',
|
|
91
|
+
'rpm',
|
|
92
|
+
'scf',
|
|
93
|
+
'scr',
|
|
94
|
+
'sct',
|
|
95
|
+
'sh',
|
|
96
|
+
'shb',
|
|
97
|
+
'shs',
|
|
98
|
+
'url',
|
|
99
|
+
'vb',
|
|
100
|
+
'vbe',
|
|
101
|
+
'vbs',
|
|
102
|
+
'vsmacros',
|
|
103
|
+
'vsw',
|
|
104
|
+
'ws',
|
|
105
|
+
'wsc',
|
|
106
|
+
'wsf',
|
|
107
|
+
'wsh',
|
|
108
|
+
'xll',
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* A filename ending in something that looks like a document extension followed
|
|
112
|
+
* by an executable one — `invoice.pdf.exe`. Mail clients that hide known
|
|
113
|
+
* extensions render it as `invoice.pdf`.
|
|
114
|
+
*/
|
|
115
|
+
const DOUBLE_EXTENSION_BAIT = /\.(pdf|docx?|xlsx?|pptx?|txt|csv|jpe?g|png|gif|zip|rtf|odt|ods)\.[a-z0-9]{1,5}$/i;
|
|
116
|
+
/**
|
|
117
|
+
* Strips a filename down to something safe to print and to reason about.
|
|
118
|
+
*
|
|
119
|
+
* The name is never used to open a file — nothing here writes to disk — but it
|
|
120
|
+
* does reach the model, and a name carrying directional overrides or path
|
|
121
|
+
* separators is trying to be read as something it is not.
|
|
122
|
+
*/
|
|
123
|
+
export function sanitizeFilename(raw) {
|
|
124
|
+
if (raw === undefined || raw.trim() === '')
|
|
125
|
+
return '(unnamed)';
|
|
126
|
+
const cleaned = defuseAutoFetch(stripInvisible(raw.normalize('NFKC')))
|
|
127
|
+
.replace(/[/\\]/g, '_')
|
|
128
|
+
.replace(/^\.+/, '')
|
|
129
|
+
.trim();
|
|
130
|
+
if (cleaned === '')
|
|
131
|
+
return '(unnamed)';
|
|
132
|
+
return cleaned.length > MAX_FILENAME_LENGTH
|
|
133
|
+
? `${cleaned.slice(0, MAX_FILENAME_LENGTH)}…`
|
|
134
|
+
: cleaned;
|
|
135
|
+
}
|
|
136
|
+
export function extensionOf(filename) {
|
|
137
|
+
const match = /\.([A-Za-z0-9]{1,10})$/.exec(filename);
|
|
138
|
+
return match?.[1]?.toLowerCase() ?? '';
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Walks the MIME tree and returns every part that is an attachment.
|
|
142
|
+
*
|
|
143
|
+
* Parts inside a forwarded `message/rfc822` are included: an attachment that
|
|
144
|
+
* arrives one level deeper is exactly as dangerous, and a listing that stops at
|
|
145
|
+
* the outer envelope would report "no attachments" for the most common way of
|
|
146
|
+
* passing a file along.
|
|
147
|
+
*/
|
|
148
|
+
export function collectAttachments(structure) {
|
|
149
|
+
const found = [];
|
|
150
|
+
walk(structure, 0, found);
|
|
151
|
+
return found.slice(0, MAX_ATTACHMENTS);
|
|
152
|
+
}
|
|
153
|
+
function walk(node, depth, found) {
|
|
154
|
+
if (node === undefined ||
|
|
155
|
+
depth > MAX_DEPTH ||
|
|
156
|
+
found.length >= MAX_ATTACHMENTS)
|
|
157
|
+
return;
|
|
158
|
+
if (node.childNodes !== undefined && node.childNodes.length > 0) {
|
|
159
|
+
for (const child of node.childNodes)
|
|
160
|
+
walk(child, depth + 1, found);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const type = (node.type ?? 'application/octet-stream').toLowerCase();
|
|
164
|
+
const disposition = node.disposition?.toLowerCase();
|
|
165
|
+
const declaredName = node.dispositionParameters?.filename ?? node.parameters?.name;
|
|
166
|
+
// A part is an attachment when it says so, or when it carries a filename, or
|
|
167
|
+
// when it is simply not one of the two body types. Inline images count: they
|
|
168
|
+
// are still bytes from a stranger.
|
|
169
|
+
const isBody = (type === 'text/plain' || type === 'text/html') &&
|
|
170
|
+
disposition !== 'attachment' &&
|
|
171
|
+
declaredName === undefined;
|
|
172
|
+
if (isBody || node.part === undefined)
|
|
173
|
+
return;
|
|
174
|
+
const filename = sanitizeFilename(declaredName);
|
|
175
|
+
const notes = [];
|
|
176
|
+
if (declaredName !== undefined && DOUBLE_EXTENSION_BAIT.test(declaredName)) {
|
|
177
|
+
notes.push('filename has a double extension — it renders as a document but is not one');
|
|
178
|
+
}
|
|
179
|
+
found.push({
|
|
180
|
+
partId: node.part,
|
|
181
|
+
filename,
|
|
182
|
+
contentType: type,
|
|
183
|
+
size: node.size,
|
|
184
|
+
disposition,
|
|
185
|
+
allowed: true,
|
|
186
|
+
notes,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Applies the declaration-level policy.
|
|
191
|
+
*
|
|
192
|
+
* Everything checked here comes from the sender, so a pass means only "nothing
|
|
193
|
+
* in what it claims about itself disqualifies it". The bytes are checked
|
|
194
|
+
* separately in {@link sniffContent} when they are actually fetched.
|
|
195
|
+
*/
|
|
196
|
+
export function checkPolicy(candidate, policy) {
|
|
197
|
+
const notes = [...candidate.notes];
|
|
198
|
+
let allowed = true;
|
|
199
|
+
const extension = extensionOf(candidate.filename);
|
|
200
|
+
if (extension !== '' && EXECUTABLE_EXTENSIONS.has(extension)) {
|
|
201
|
+
allowed = false;
|
|
202
|
+
notes.push(`refused: .${extension} is an executable file type`);
|
|
203
|
+
}
|
|
204
|
+
if (!policy.allowedTypes.includes(candidate.contentType)) {
|
|
205
|
+
allowed = false;
|
|
206
|
+
notes.push(`refused: content type ${candidate.contentType} is not in the allowlist (IMAP_ATTACHMENT_TYPES)`);
|
|
207
|
+
}
|
|
208
|
+
if (candidate.size !== undefined && candidate.size > policy.maxBytes) {
|
|
209
|
+
allowed = false;
|
|
210
|
+
notes.push(`refused: declared size ${candidate.size} exceeds IMAP_MAX_ATTACHMENT_BYTES (${policy.maxBytes})`);
|
|
211
|
+
}
|
|
212
|
+
return { ...candidate, allowed, notes };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Identifies content by its leading bytes.
|
|
216
|
+
*
|
|
217
|
+
* The declared content type is a claim by the sender; this is the only check
|
|
218
|
+
* that looks at what was actually sent. An executable renamed to `.txt` and
|
|
219
|
+
* declared as `text/plain` passes every other gate and fails here.
|
|
220
|
+
*/
|
|
221
|
+
export function sniffContent(buffer) {
|
|
222
|
+
const byte = (i) => buffer[i] ?? -1;
|
|
223
|
+
if (byte(0) === 0x4d && byte(1) === 0x5a) {
|
|
224
|
+
return { executable: true, detectedType: 'application/x-msdownload' };
|
|
225
|
+
}
|
|
226
|
+
if (byte(0) === 0x7f &&
|
|
227
|
+
byte(1) === 0x45 &&
|
|
228
|
+
byte(2) === 0x4c &&
|
|
229
|
+
byte(3) === 0x46) {
|
|
230
|
+
return { executable: true, detectedType: 'application/x-elf' };
|
|
231
|
+
}
|
|
232
|
+
// `>>> 0` is load-bearing: JS bitwise operators produce a *signed* 32-bit
|
|
233
|
+
// result, so without it 0xfeedface arrives as a negative number and none of
|
|
234
|
+
// the Mach-O comparisons below can ever match.
|
|
235
|
+
const magic32 = ((byte(0) << 24) | (byte(1) << 16) | (byte(2) << 8) | byte(3)) >>> 0;
|
|
236
|
+
if (magic32 === 0xfeedface ||
|
|
237
|
+
magic32 === 0xfeedfacf ||
|
|
238
|
+
magic32 === 0xcafebabe) {
|
|
239
|
+
return { executable: true, detectedType: 'application/x-mach-binary' };
|
|
240
|
+
}
|
|
241
|
+
if (byte(0) === 0x23 && byte(1) === 0x21) {
|
|
242
|
+
return { executable: true, detectedType: 'text/x-shellscript' };
|
|
243
|
+
}
|
|
244
|
+
const starts = (signature) => signature.every((value, index) => byte(index) === value);
|
|
245
|
+
if (starts([0x25, 0x50, 0x44, 0x46])) {
|
|
246
|
+
return { executable: false, detectedType: 'application/pdf' };
|
|
247
|
+
}
|
|
248
|
+
if (starts([0x89, 0x50, 0x4e, 0x47])) {
|
|
249
|
+
return { executable: false, detectedType: 'image/png' };
|
|
250
|
+
}
|
|
251
|
+
if (starts([0xff, 0xd8, 0xff])) {
|
|
252
|
+
return { executable: false, detectedType: 'image/jpeg' };
|
|
253
|
+
}
|
|
254
|
+
if (starts([0x47, 0x49, 0x46, 0x38])) {
|
|
255
|
+
return { executable: false, detectedType: 'image/gif' };
|
|
256
|
+
}
|
|
257
|
+
if (starts([0x52, 0x49, 0x46, 0x46]) &&
|
|
258
|
+
byte(8) === 0x57 &&
|
|
259
|
+
byte(9) === 0x45 &&
|
|
260
|
+
byte(10) === 0x42 &&
|
|
261
|
+
byte(11) === 0x50) {
|
|
262
|
+
return { executable: false, detectedType: 'image/webp' };
|
|
263
|
+
}
|
|
264
|
+
if (starts([0x50, 0x4b, 0x03, 0x04])) {
|
|
265
|
+
// Also every modern Office and OpenDocument file.
|
|
266
|
+
return { executable: false, detectedType: 'application/zip' };
|
|
267
|
+
}
|
|
268
|
+
return { executable: false, detectedType: undefined };
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=attachments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records every change this server makes to the mailbox.
|
|
3
|
+
*
|
|
4
|
+
* The line goes to stderr, which is the one channel the model never reads: the
|
|
5
|
+
* MCP transport owns stdout, and tool results go back to the model. So this is
|
|
6
|
+
* the only place a human can afterwards reconstruct what a hijacked session
|
|
7
|
+
* actually did — the model's own account of events is not evidence.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately no subjects, senders or bodies. An audit line that quotes the
|
|
10
|
+
* mail would put attacker-chosen text into the operator's log viewer, and the
|
|
11
|
+
* facts needed to undo something are the UIDs and the folder, not the prose.
|
|
12
|
+
* The one exception is the path of a saved attachment: its filename was chosen
|
|
13
|
+
* by the sender, but it has been through sanitizeFilename — no control or
|
|
14
|
+
* invisible characters, no separators — and without it the line could not say
|
|
15
|
+
* which file to go and delete.
|
|
16
|
+
*/
|
|
17
|
+
export declare function audit(operation: string, details: Record<string, unknown>): void;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records every change this server makes to the mailbox.
|
|
3
|
+
*
|
|
4
|
+
* The line goes to stderr, which is the one channel the model never reads: the
|
|
5
|
+
* MCP transport owns stdout, and tool results go back to the model. So this is
|
|
6
|
+
* the only place a human can afterwards reconstruct what a hijacked session
|
|
7
|
+
* actually did — the model's own account of events is not evidence.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately no subjects, senders or bodies. An audit line that quotes the
|
|
10
|
+
* mail would put attacker-chosen text into the operator's log viewer, and the
|
|
11
|
+
* facts needed to undo something are the UIDs and the folder, not the prose.
|
|
12
|
+
* The one exception is the path of a saved attachment: its filename was chosen
|
|
13
|
+
* by the sender, but it has been through sanitizeFilename — no control or
|
|
14
|
+
* invisible characters, no separators — and without it the line could not say
|
|
15
|
+
* which file to go and delete.
|
|
16
|
+
*/
|
|
17
|
+
export function audit(operation, details) {
|
|
18
|
+
const fields = Object.entries(details)
|
|
19
|
+
.filter(([, value]) => value !== undefined)
|
|
20
|
+
.map(([key, value]) => `${key}=${format(value)}`)
|
|
21
|
+
.join(' ');
|
|
22
|
+
console.error(`imap-mcp audit ${new Date().toISOString()} ${operation} ${fields}`);
|
|
23
|
+
}
|
|
24
|
+
/** Long UID lists are abbreviated so one bulk call cannot flood the log. */
|
|
25
|
+
function format(value) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.length > 20
|
|
28
|
+
? `[${value.slice(0, 20).join(',')},…+${value.length - 20}]`
|
|
29
|
+
: `[${value.join(',')}]`;
|
|
30
|
+
}
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,KAAK,CACnB,SAAiB,EACjB,OAAgC;IAEhC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;SACnC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;SAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;SAChD,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,CAAC,KAAK,CACX,kBAAkB,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,SAAS,IAAI,MAAM,EAAE,CACpE,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,MAAM,GAAG,EAAE;YACtB,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,GAAG;YAC5D,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC7B,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** How the IMAP connection is encrypted. */
|
|
2
|
+
export type TlsMode = 'implicit' | 'starttls' | 'none';
|
|
3
|
+
export interface ImapConfig {
|
|
4
|
+
host: string | undefined;
|
|
5
|
+
port: number;
|
|
6
|
+
user: string | undefined;
|
|
7
|
+
password: string | undefined;
|
|
8
|
+
tls: TlsMode;
|
|
9
|
+
insecureTls: boolean;
|
|
10
|
+
/** Mailbox the message tools default to. */
|
|
11
|
+
mailbox: string;
|
|
12
|
+
/**
|
|
13
|
+
* Custom IMAP keyword marking messages already handed to the model. Empty
|
|
14
|
+
* means the feature is off and `list_new_messages` is not registered.
|
|
15
|
+
*/
|
|
16
|
+
seenKeyword: string;
|
|
17
|
+
/** Overrides the folder auto-detected from the \Drafts special-use flag. */
|
|
18
|
+
draftsMailbox: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The authserv-id this account's own provider stamps into
|
|
21
|
+
* Authentication-Results. Only a header carrying it is treated as
|
|
22
|
+
* non-forgeable; unset, every SPF/DKIM/DMARC verdict is reported as forgeable,
|
|
23
|
+
* because a sender can write that header too and nothing in the message
|
|
24
|
+
* distinguishes theirs from the provider's.
|
|
25
|
+
*/
|
|
26
|
+
trustedAuthservId: string | undefined;
|
|
27
|
+
maxMessages: number;
|
|
28
|
+
maxAttachmentBytes: number;
|
|
29
|
+
allowedAttachmentTypes: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Where attachments may be written. Unset means this server never touches the
|
|
32
|
+
* filesystem — setting it is the opt-in, and it is the only source of the
|
|
33
|
+
* target directory. A caller cannot choose where bytes from a stranger land.
|
|
34
|
+
*/
|
|
35
|
+
downloadDir: string | undefined;
|
|
36
|
+
maxDownloadBytes: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Config {
|
|
39
|
+
imap: ImapConfig;
|
|
40
|
+
/**
|
|
41
|
+
* When true — the default — the mailbox write tools are not registered.
|
|
42
|
+
*
|
|
43
|
+
* Note the default, which is the opposite of every other server in this
|
|
44
|
+
* family. This variable replaced `IMAP_ALLOW_WRITE`, and that one was opt-in:
|
|
45
|
+
* setting nothing meant no write access to a mailbox. Renaming it without
|
|
46
|
+
* keeping that default would have handed write access to every installation
|
|
47
|
+
* that upgraded without reading the changelog. The name is now shared with
|
|
48
|
+
* the rest of the family; the default deliberately is not.
|
|
49
|
+
*/
|
|
50
|
+
readOnly: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Raw value of `IMAP_ALLOW_TOOLS` — comma-separated tool names, `list_*`
|
|
53
|
+
* prefixes, or `essential`. Kept unparsed on purpose: this file is a mirror
|
|
54
|
+
* of the environment, and the names can only be checked against the tool
|
|
55
|
+
* catalogue, which `buildToolFilter` does.
|
|
56
|
+
*/
|
|
57
|
+
allowTools: string | undefined;
|
|
58
|
+
/** Raw value of `IMAP_DENY_TOOLS`, same shape, subtracted from the above. */
|
|
59
|
+
denyTools: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
export declare const DEFAULT_ATTACHMENT_TYPES: string[];
|
|
62
|
+
/** Shown when the configuration is incomplete — at startup and on every call. */
|
|
63
|
+
export declare function missingConfigMessage(missing: string[]): string;
|
|
64
|
+
/** Names of the required environment variables that are unset in `config`. */
|
|
65
|
+
export declare function missingConfigKeys(config: Config): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Reads the configuration from environment variables.
|
|
68
|
+
*
|
|
69
|
+
* Missing credentials are only a warning, not a fatal error: the server must be
|
|
70
|
+
* able to complete the MCP handshake and answer `tools/list` without them, so
|
|
71
|
+
* registries and sandbox inspectors can introspect it. Malformed values still
|
|
72
|
+
* exit — a host with a newline in it could smuggle a second command into the
|
|
73
|
+
* IMAP session, and a bad port would connect somewhere unintended.
|
|
74
|
+
*/
|
|
75
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export const DEFAULT_ATTACHMENT_TYPES = [
|
|
2
|
+
'application/pdf',
|
|
3
|
+
'application/json',
|
|
4
|
+
'application/xml',
|
|
5
|
+
'application/zip',
|
|
6
|
+
'application/rtf',
|
|
7
|
+
'application/vnd.oasis.opendocument.text',
|
|
8
|
+
'application/vnd.oasis.opendocument.spreadsheet',
|
|
9
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
10
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
11
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
12
|
+
'image/png',
|
|
13
|
+
'image/jpeg',
|
|
14
|
+
'image/gif',
|
|
15
|
+
'image/webp',
|
|
16
|
+
'text/plain',
|
|
17
|
+
'text/csv',
|
|
18
|
+
'text/html',
|
|
19
|
+
'text/calendar',
|
|
20
|
+
];
|
|
21
|
+
const DEFAULT_MAX_MESSAGES = 100;
|
|
22
|
+
/** Inline cap: this one protects the model's context window. */
|
|
23
|
+
const DEFAULT_MAX_ATTACHMENT_BYTES = 1024 * 1024;
|
|
24
|
+
/** Disk cap: this one protects the filesystem, which is a different concern. */
|
|
25
|
+
const DEFAULT_MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
|
|
26
|
+
const DEFAULT_SEEN_KEYWORD = 'AiSeen';
|
|
27
|
+
/** Shown when the configuration is incomplete — at startup and on every call. */
|
|
28
|
+
export function missingConfigMessage(missing) {
|
|
29
|
+
return (`missing required environment variable(s): ${missing.join(', ')}\n` +
|
|
30
|
+
'Required: IMAP_HOST (e.g. imap.example.net), IMAP_USER, IMAP_PASSWORD\n' +
|
|
31
|
+
'Optional: IMAP_PORT, IMAP_TLS (implicit|starttls|none), IMAP_MAILBOX, ' +
|
|
32
|
+
'IMAP_SEEN_KEYWORD, IMAP_READ_ONLY=false to expose the mailbox write ' +
|
|
33
|
+
'tools (it defaults to true), IMAP_ALLOW_TOOLS / IMAP_DENY_TOOLS to narrow ' +
|
|
34
|
+
'the tool list, IMAP_DOWNLOAD_DIR to allow saving attachments to disk, ' +
|
|
35
|
+
'IMAP_INSECURE_TLS=true to accept self-signed certificates');
|
|
36
|
+
}
|
|
37
|
+
/** Names of the required environment variables that are unset in `config`. */
|
|
38
|
+
export function missingConfigKeys(config) {
|
|
39
|
+
return [
|
|
40
|
+
!config.imap.host && 'IMAP_HOST',
|
|
41
|
+
!config.imap.user && 'IMAP_USER',
|
|
42
|
+
!config.imap.password && 'IMAP_PASSWORD',
|
|
43
|
+
].filter((v) => Boolean(v));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the configuration from environment variables.
|
|
47
|
+
*
|
|
48
|
+
* Missing credentials are only a warning, not a fatal error: the server must be
|
|
49
|
+
* able to complete the MCP handshake and answer `tools/list` without them, so
|
|
50
|
+
* registries and sandbox inspectors can introspect it. Malformed values still
|
|
51
|
+
* exit — a host with a newline in it could smuggle a second command into the
|
|
52
|
+
* IMAP session, and a bad port would connect somewhere unintended.
|
|
53
|
+
*/
|
|
54
|
+
export function loadConfig(env = process.env) {
|
|
55
|
+
const host = env.IMAP_HOST;
|
|
56
|
+
const user = env.IMAP_USER;
|
|
57
|
+
const password = env.IMAP_PASSWORD;
|
|
58
|
+
// Removed here, before any branch below can return or exit: the password must
|
|
59
|
+
// not stay in the environment for the process lifetime, where it is visible
|
|
60
|
+
// to child processes and in /proc/<pid>/environ.
|
|
61
|
+
delete env.IMAP_PASSWORD;
|
|
62
|
+
const tls = parseTlsMode(env.IMAP_TLS);
|
|
63
|
+
if (host !== undefined)
|
|
64
|
+
assertSafeHost(host, 'IMAP_HOST');
|
|
65
|
+
const draftsMailbox = env.IMAP_DRAFTS_MAILBOX;
|
|
66
|
+
if (draftsMailbox !== undefined) {
|
|
67
|
+
assertSingleLine(draftsMailbox, 'IMAP_DRAFTS_MAILBOX');
|
|
68
|
+
}
|
|
69
|
+
const trustedAuthservId = env.IMAP_TRUSTED_AUTHSERV_ID?.trim() || undefined;
|
|
70
|
+
if (trustedAuthservId !== undefined) {
|
|
71
|
+
assertSingleLine(trustedAuthservId, 'IMAP_TRUSTED_AUTHSERV_ID');
|
|
72
|
+
}
|
|
73
|
+
const config = {
|
|
74
|
+
imap: {
|
|
75
|
+
host,
|
|
76
|
+
port: parsePort(env.IMAP_PORT, tls === 'implicit' ? 993 : 143, 'IMAP_PORT'),
|
|
77
|
+
user,
|
|
78
|
+
password,
|
|
79
|
+
tls,
|
|
80
|
+
insecureTls: env.IMAP_INSECURE_TLS === 'true',
|
|
81
|
+
mailbox: env.IMAP_MAILBOX || 'INBOX',
|
|
82
|
+
seenKeyword: parseKeyword(env.IMAP_SEEN_KEYWORD),
|
|
83
|
+
draftsMailbox,
|
|
84
|
+
trustedAuthservId,
|
|
85
|
+
maxMessages: parseCount(env.IMAP_MAX_MESSAGES, DEFAULT_MAX_MESSAGES, 'IMAP_MAX_MESSAGES'),
|
|
86
|
+
maxAttachmentBytes: parseCount(env.IMAP_MAX_ATTACHMENT_BYTES, DEFAULT_MAX_ATTACHMENT_BYTES, 'IMAP_MAX_ATTACHMENT_BYTES'),
|
|
87
|
+
allowedAttachmentTypes: parseTypes(env.IMAP_ATTACHMENT_TYPES),
|
|
88
|
+
downloadDir: env.IMAP_DOWNLOAD_DIR,
|
|
89
|
+
maxDownloadBytes: parseCount(env.IMAP_MAX_DOWNLOAD_BYTES, DEFAULT_MAX_DOWNLOAD_BYTES, 'IMAP_MAX_DOWNLOAD_BYTES'),
|
|
90
|
+
},
|
|
91
|
+
// Defaults to true, unlike the rest of the family — see the field comment.
|
|
92
|
+
readOnly: env.IMAP_READ_ONLY !== 'false',
|
|
93
|
+
allowTools: env.IMAP_ALLOW_TOOLS,
|
|
94
|
+
denyTools: env.IMAP_DENY_TOOLS,
|
|
95
|
+
};
|
|
96
|
+
// Silently ignoring a removed security variable is the worst of the options:
|
|
97
|
+
// whoever set it once believes it is still in force. IMAP_ALLOW_WRITE=true
|
|
98
|
+
// used to be the only way to reach the write tools, so an installation that
|
|
99
|
+
// still sets it is one that wants them — and would otherwise get a read-only
|
|
100
|
+
// server without being told why.
|
|
101
|
+
if (env.IMAP_ALLOW_WRITE !== undefined) {
|
|
102
|
+
console.error('imap-mcp: IMAP_ALLOW_WRITE has been replaced by IMAP_READ_ONLY. Set ' +
|
|
103
|
+
'IMAP_READ_ONLY=false for the write tools, or unset IMAP_ALLOW_WRITE ' +
|
|
104
|
+
'to keep the read-only default.');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const missing = missingConfigKeys(config);
|
|
108
|
+
if (missing.length > 0) {
|
|
109
|
+
console.error(`imap-mcp: ${missingConfigMessage(missing)}`);
|
|
110
|
+
}
|
|
111
|
+
if (config.imap.tls === 'none' && !isLoopbackHost(host)) {
|
|
112
|
+
console.error('imap-mcp: WARNING: IMAP_TLS=none against a non-local host — the password ' +
|
|
113
|
+
'and every message will cross the network unencrypted.');
|
|
114
|
+
}
|
|
115
|
+
return config;
|
|
116
|
+
}
|
|
117
|
+
function parseTlsMode(raw) {
|
|
118
|
+
if (raw === undefined || raw === '')
|
|
119
|
+
return 'implicit';
|
|
120
|
+
if (raw === 'implicit' || raw === 'starttls' || raw === 'none')
|
|
121
|
+
return raw;
|
|
122
|
+
console.error('imap-mcp: IMAP_TLS must be one of implicit, starttls or none');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
function parsePort(raw, fallback, name) {
|
|
126
|
+
if (raw === undefined || raw === '')
|
|
127
|
+
return fallback;
|
|
128
|
+
const value = Number(raw);
|
|
129
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
130
|
+
// The value itself is not echoed: config errors end up in logs.
|
|
131
|
+
console.error(`imap-mcp: ${name} must be an integer between 1 and 65535`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function parseCount(raw, fallback, name) {
|
|
137
|
+
if (raw === undefined || raw === '')
|
|
138
|
+
return fallback;
|
|
139
|
+
const value = Number(raw);
|
|
140
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
141
|
+
console.error(`imap-mcp: ${name} must be a positive integer`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* An IMAP keyword is an atom: no spaces and none of the characters that would
|
|
148
|
+
* end it early or open a literal. Empty turns the new-mail tracking off.
|
|
149
|
+
*/
|
|
150
|
+
function parseKeyword(raw) {
|
|
151
|
+
if (raw === undefined)
|
|
152
|
+
return DEFAULT_SEEN_KEYWORD;
|
|
153
|
+
if (raw === '')
|
|
154
|
+
return '';
|
|
155
|
+
if (!/^[A-Za-z0-9$_.-]+$/.test(raw)) {
|
|
156
|
+
console.error('imap-mcp: IMAP_SEEN_KEYWORD must consist of letters, digits, $, _, . or -');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
return raw;
|
|
160
|
+
}
|
|
161
|
+
function parseTypes(raw) {
|
|
162
|
+
if (raw === undefined || raw.trim() === '')
|
|
163
|
+
return DEFAULT_ATTACHMENT_TYPES;
|
|
164
|
+
return raw
|
|
165
|
+
.split(',')
|
|
166
|
+
.map((t) => t.trim().toLowerCase())
|
|
167
|
+
.filter((t) => t !== '');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Rejects anything that could break out of the line it is written on. IMAP is a
|
|
171
|
+
* line protocol; a CR or LF in a hostname is a command-smuggling primitive, not
|
|
172
|
+
* a typo.
|
|
173
|
+
*/
|
|
174
|
+
function assertSafeHost(value, name) {
|
|
175
|
+
// A hostname or IPv4 address — or an IPv6 address, which is the only place
|
|
176
|
+
// a colon is legal. Allowing ":" everywhere would silently accept
|
|
177
|
+
// "imap.example.net:993", which the error message promises to reject.
|
|
178
|
+
const hostname = /^[A-Za-z0-9._-]+$/.test(value);
|
|
179
|
+
const ipv6 = /^\[?[0-9A-Fa-f:.]*:[0-9A-Fa-f:.]*\]?$/.test(value);
|
|
180
|
+
if (!hostname && !ipv6) {
|
|
181
|
+
console.error(`imap-mcp: ${name} must be a plain hostname or IP address without ` +
|
|
182
|
+
'scheme, port, credentials or whitespace');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Spaces are fine in a mailbox name; a line break would end the command. */
|
|
187
|
+
function assertSingleLine(value, name) {
|
|
188
|
+
if (/[\r\n]/.test(value)) {
|
|
189
|
+
console.error(`imap-mcp: ${name} must not contain line breaks`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function isLoopbackHost(hostname) {
|
|
194
|
+
if (hostname === undefined)
|
|
195
|
+
return false;
|
|
196
|
+
return (hostname === 'localhost' ||
|
|
197
|
+
hostname.endsWith('.localhost') ||
|
|
198
|
+
hostname.startsWith('127.') ||
|
|
199
|
+
hostname === '::1' ||
|
|
200
|
+
hostname === '[::1]');
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|