@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
package/dist/imap.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { ImapFlow } from 'imapflow';
|
|
2
|
+
import { missingConfigKeys, missingConfigMessage, } from './config.js';
|
|
3
|
+
import { MailError, ToolInputError } from './errors.js';
|
|
4
|
+
const defaultFactory = (config) => new ImapFlow({
|
|
5
|
+
host: config.host ?? '',
|
|
6
|
+
port: config.port,
|
|
7
|
+
secure: config.tls === 'implicit',
|
|
8
|
+
// Left unset, imapflow upgrades opportunistically — which means a
|
|
9
|
+
// downgrade attack succeeds silently. Both non-implicit modes say
|
|
10
|
+
// explicitly which one they meant.
|
|
11
|
+
doSTARTTLS: config.tls === 'starttls',
|
|
12
|
+
auth: { user: config.user ?? '', pass: config.password ?? '' },
|
|
13
|
+
// The library logs full IMAP traffic — message bodies included — to stdout
|
|
14
|
+
// by default. stdout is the MCP transport, so this is not optional.
|
|
15
|
+
logger: false,
|
|
16
|
+
// Scoped to this connection: NODE_TLS_REJECT_UNAUTHORIZED would disable
|
|
17
|
+
// certificate checking for the whole process, SMTP included.
|
|
18
|
+
tls: config.insecureTls ? { rejectUnauthorized: false } : {},
|
|
19
|
+
});
|
|
20
|
+
/** How long a single IMAP command may take before the call is abandoned. */
|
|
21
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
22
|
+
/**
|
|
23
|
+
* Connection manager around a single IMAP account.
|
|
24
|
+
*
|
|
25
|
+
* Reads take the mailbox lock read-only and use BODY.PEEK throughout, so
|
|
26
|
+
* fetching a message never changes what the human sees as unread. The only
|
|
27
|
+
* exception is deliberate and explicit: {@link tagSeen}, which writes the
|
|
28
|
+
* server's own bookkeeping keyword.
|
|
29
|
+
*/
|
|
30
|
+
export class ImapClient {
|
|
31
|
+
config;
|
|
32
|
+
factory;
|
|
33
|
+
connection;
|
|
34
|
+
connecting;
|
|
35
|
+
constructor(config, factory = defaultFactory) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.factory = factory;
|
|
38
|
+
}
|
|
39
|
+
/** Credentials are checked per call, not at startup. */
|
|
40
|
+
assertConfigured() {
|
|
41
|
+
const missing = missingConfigKeys(this.config);
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
throw new Error(missingConfigMessage(missing));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async connection_() {
|
|
47
|
+
this.assertConfigured();
|
|
48
|
+
if (this.connection !== undefined)
|
|
49
|
+
return this.connection;
|
|
50
|
+
if (this.connecting !== undefined)
|
|
51
|
+
return this.connecting;
|
|
52
|
+
this.connecting = (async () => {
|
|
53
|
+
const client = this.factory(this.config.imap);
|
|
54
|
+
try {
|
|
55
|
+
await client.connect();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.connecting = undefined;
|
|
59
|
+
throw asMailError(error);
|
|
60
|
+
}
|
|
61
|
+
this.connection = client;
|
|
62
|
+
this.connecting = undefined;
|
|
63
|
+
return client;
|
|
64
|
+
})();
|
|
65
|
+
return this.connecting;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Runs `fn` with the mailbox open and the lock held.
|
|
69
|
+
*
|
|
70
|
+
* A dropped connection is retried exactly once: IMAP sessions are long-lived
|
|
71
|
+
* and idle ones get reaped by servers and NAT gateways alike, so the first
|
|
72
|
+
* call after a pause routinely fails for reasons that have nothing to do with
|
|
73
|
+
* the request.
|
|
74
|
+
*/
|
|
75
|
+
async withMailbox(mailbox, readOnly, fn) {
|
|
76
|
+
const path = mailbox ?? this.config.imap.mailbox;
|
|
77
|
+
try {
|
|
78
|
+
return await this.run(path, readOnly, fn);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isConnectionError(error))
|
|
82
|
+
throw error;
|
|
83
|
+
this.forget();
|
|
84
|
+
return await this.run(path, readOnly, fn);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async run(path, readOnly, fn) {
|
|
88
|
+
const client = await this.connection_();
|
|
89
|
+
let lock;
|
|
90
|
+
try {
|
|
91
|
+
lock = await client.getMailboxLock(path, { readOnly });
|
|
92
|
+
// A NOOP before SEARCH: without it the server is free to keep reporting
|
|
93
|
+
// the mailbox as it looked when the connection was opened, and mail that
|
|
94
|
+
// arrived since then stays invisible.
|
|
95
|
+
await withTimeout(client.noop(), 'NOOP');
|
|
96
|
+
return await fn(client, path);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw asMailError(error);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
lock?.release();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Runs `fn` without selecting a mailbox, for LIST/STATUS/CREATE style calls. */
|
|
106
|
+
async withConnection(fn) {
|
|
107
|
+
try {
|
|
108
|
+
return await fn(await this.connection_());
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (!isConnectionError(error))
|
|
112
|
+
throw asMailError(error);
|
|
113
|
+
this.forget();
|
|
114
|
+
try {
|
|
115
|
+
return await fn(await this.connection_());
|
|
116
|
+
}
|
|
117
|
+
catch (retryError) {
|
|
118
|
+
throw asMailError(retryError);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
forget() {
|
|
123
|
+
try {
|
|
124
|
+
this.connection?.close();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Already gone; nothing to clean up.
|
|
128
|
+
}
|
|
129
|
+
this.connection = undefined;
|
|
130
|
+
}
|
|
131
|
+
async listMailboxes() {
|
|
132
|
+
return this.withConnection(async (client) => {
|
|
133
|
+
// statusQuery folds the per-folder counters into the same round trip, so
|
|
134
|
+
// list_mailboxes can answer "which folder, and how much is in it" at once.
|
|
135
|
+
const entries = await withTimeout(client.list({
|
|
136
|
+
statusQuery: { messages: true, unseen: true, uidNext: true },
|
|
137
|
+
}), 'LIST');
|
|
138
|
+
return entries.map((entry) => ({
|
|
139
|
+
path: entry.path,
|
|
140
|
+
name: entry.name,
|
|
141
|
+
delimiter: entry.delimiter,
|
|
142
|
+
specialUse: entry.specialUse,
|
|
143
|
+
subscribed: entry.subscribed,
|
|
144
|
+
selectable: !entry.flags.has('\\Noselect'),
|
|
145
|
+
messages: entry.status?.messages,
|
|
146
|
+
unseen: entry.status?.unseen,
|
|
147
|
+
uidNext: entry.status?.uidNext,
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether the account can store the bookkeeping keyword.
|
|
153
|
+
*
|
|
154
|
+
* `\*` in PERMANENTFLAGS means the server accepts arbitrary keywords. Some
|
|
155
|
+
* providers accept none at all, and there the new-mail tracking cannot work —
|
|
156
|
+
* better to say so than to tag silently into the void.
|
|
157
|
+
*/
|
|
158
|
+
keywordSupported(permanentFlags) {
|
|
159
|
+
const keyword = this.config.imap.seenKeyword;
|
|
160
|
+
if (keyword === '')
|
|
161
|
+
return false;
|
|
162
|
+
return permanentFlags.has('\\*') || permanentFlags.has(keyword);
|
|
163
|
+
}
|
|
164
|
+
get seenKeyword() {
|
|
165
|
+
return this.config.imap.seenKeyword;
|
|
166
|
+
}
|
|
167
|
+
get defaultMailbox() {
|
|
168
|
+
return this.config.imap.mailbox;
|
|
169
|
+
}
|
|
170
|
+
get maxMessages() {
|
|
171
|
+
return this.config.imap.maxMessages;
|
|
172
|
+
}
|
|
173
|
+
get user() {
|
|
174
|
+
return this.config.imap.user;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Reads the threading headers of one message, for linking a draft into an
|
|
178
|
+
* existing conversation.
|
|
179
|
+
*
|
|
180
|
+
* Only the Message-ID and the References chain are taken; nothing the sender
|
|
181
|
+
* wrote as prose comes back from here.
|
|
182
|
+
*/
|
|
183
|
+
async threadHeaders(mailbox, uid) {
|
|
184
|
+
return this.withMailbox(mailbox, true, async (connection) => {
|
|
185
|
+
for await (const message of connection.fetch([uid], { uid: true, envelope: true, headers: ['references', 'in-reply-to'] }, { uid: true })) {
|
|
186
|
+
const raw = message.headers?.toString('utf-8') ?? '';
|
|
187
|
+
const ids = raw.match(/<[^\s<>]{1,255}>/g) ?? [];
|
|
188
|
+
const messageId = message.envelope?.messageId;
|
|
189
|
+
const chain = [...ids, ...(messageId === undefined ? [] : [messageId])];
|
|
190
|
+
return {
|
|
191
|
+
messageId,
|
|
192
|
+
// Bounded: a long-running thread accumulates dozens of ids and each
|
|
193
|
+
// one lengthens the header we are about to write.
|
|
194
|
+
references: [...new Set(chain)].slice(-20),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
throw new ToolInputError(`imap-mcp: no message with UID ${uid} in this mailbox.`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async search(client, query) {
|
|
201
|
+
const found = await withTimeout(client.search(query, { uid: true }), 'SEARCH');
|
|
202
|
+
return found === false ? [] : found;
|
|
203
|
+
}
|
|
204
|
+
/** Envelope-level fetch, newest UID first. */
|
|
205
|
+
async fetchSummaries(client, uids) {
|
|
206
|
+
if (uids.length === 0)
|
|
207
|
+
return [];
|
|
208
|
+
const messages = [];
|
|
209
|
+
for await (const message of client.fetch(uids, {
|
|
210
|
+
uid: true,
|
|
211
|
+
envelope: true,
|
|
212
|
+
flags: true,
|
|
213
|
+
size: true,
|
|
214
|
+
internalDate: true,
|
|
215
|
+
}, { uid: true })) {
|
|
216
|
+
messages.push(message);
|
|
217
|
+
}
|
|
218
|
+
return messages.sort((a, b) => b.uid - a.uid);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Adds the bookkeeping keyword to the given UIDs.
|
|
222
|
+
*
|
|
223
|
+
* This is the one write the server performs on its own initiative, and it
|
|
224
|
+
* stays available under the default IMAP_READ_ONLY: without it `list_new_messages`
|
|
225
|
+
* would return the same mail forever. It touches no flag a human interacts
|
|
226
|
+
* with — `\Seen` in particular is left alone.
|
|
227
|
+
*/
|
|
228
|
+
async tagSeen(client, uids) {
|
|
229
|
+
if (uids.length === 0 || this.config.imap.seenKeyword === '')
|
|
230
|
+
return;
|
|
231
|
+
await withTimeout(client.messageFlagsAdd(uids, [this.config.imap.seenKeyword], {
|
|
232
|
+
uid: true,
|
|
233
|
+
}), 'STORE');
|
|
234
|
+
}
|
|
235
|
+
async close() {
|
|
236
|
+
if (this.connection === undefined)
|
|
237
|
+
return;
|
|
238
|
+
try {
|
|
239
|
+
await this.connection.logout();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
this.connection.close();
|
|
243
|
+
}
|
|
244
|
+
this.connection = undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Rejects a promise that outlives the command timeout. */
|
|
248
|
+
export async function withTimeout(promise, command, ms = COMMAND_TIMEOUT_MS) {
|
|
249
|
+
let timer;
|
|
250
|
+
try {
|
|
251
|
+
return await Promise.race([
|
|
252
|
+
promise,
|
|
253
|
+
new Promise((_resolve, reject) => {
|
|
254
|
+
timer = setTimeout(() => reject(new MailError(`IMAP ${command} timed out after ${ms / 1000} seconds`, 'ETIMEDOUT')), ms);
|
|
255
|
+
}),
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
if (timer !== undefined)
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const CONNECTION_ERROR_CODES = new Set([
|
|
264
|
+
'ECONNRESET',
|
|
265
|
+
'EPIPE',
|
|
266
|
+
'ETIMEDOUT',
|
|
267
|
+
'ECONNREFUSED',
|
|
268
|
+
'EHOSTUNREACH',
|
|
269
|
+
'ENOTCONN',
|
|
270
|
+
'NoConnection',
|
|
271
|
+
]);
|
|
272
|
+
function isConnectionError(error) {
|
|
273
|
+
if (error instanceof MailError) {
|
|
274
|
+
return error.code !== undefined && CONNECTION_ERROR_CODES.has(error.code);
|
|
275
|
+
}
|
|
276
|
+
const code = error?.code;
|
|
277
|
+
return typeof code === 'string' && CONNECTION_ERROR_CODES.has(code);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Normalises whatever imapflow threw into a {@link MailError}.
|
|
281
|
+
*
|
|
282
|
+
* Only the response text is carried over, never the whole error object: it
|
|
283
|
+
* holds the command that was sent, and for a LOGIN that means the password.
|
|
284
|
+
*/
|
|
285
|
+
export function asMailError(error) {
|
|
286
|
+
if (error instanceof MailError)
|
|
287
|
+
return error;
|
|
288
|
+
if (error instanceof ToolInputError)
|
|
289
|
+
throw error;
|
|
290
|
+
const source = error;
|
|
291
|
+
const message = typeof source?.message === 'string' ? source.message : String(error);
|
|
292
|
+
const code = typeof source?.code === 'string' ? source.code : undefined;
|
|
293
|
+
const responseText = typeof source?.responseText === 'string'
|
|
294
|
+
? source.responseText
|
|
295
|
+
: typeof source?.response === 'string'
|
|
296
|
+
? source.response
|
|
297
|
+
: '';
|
|
298
|
+
return new MailError(`IMAP error: ${message}`, code, responseText);
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=imap.js.map
|
package/dist/imap.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imap.js","sourceRoot":"","sources":["../src/imap.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { createServer } from './server.js';
|
|
5
|
+
import { ToolFilterError } from './tool-filter.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
if (config.imap.insecureTls) {
|
|
9
|
+
console.error('imap-mcp: IMAP_INSECURE_TLS=true — certificate validation is disabled for the mail connections');
|
|
10
|
+
}
|
|
11
|
+
if (config.readOnly) {
|
|
12
|
+
console.error('imap-mcp: IMAP_READ_ONLY is not "false" — the mailbox write tools are not registered');
|
|
13
|
+
}
|
|
14
|
+
let server;
|
|
15
|
+
try {
|
|
16
|
+
server = createServer(config);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// A bad tool list is operator feedback, not a crash.
|
|
20
|
+
if (error instanceof ToolFilterError) {
|
|
21
|
+
console.error(`imap-mcp: ${error.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
// stdout belongs to the protocol; everything human-readable goes to stderr.
|
|
27
|
+
await server.connect(new StdioServerTransport());
|
|
28
|
+
console.error(config.imap.host === undefined
|
|
29
|
+
? 'imap-mcp: connected without configuration — tools are listed but every call will fail'
|
|
30
|
+
: `imap-mcp: connected, mailbox "${config.imap.mailbox}" on ${config.imap.host}`);
|
|
31
|
+
}
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error('imap-mcp: fatal error:', error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CACX,gGAAgG,CACjG,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,CACX,sFAAsF,CACvF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,qDAAqD;QACrD,IAAI,KAAK,YAAY,eAAe,EAAE,CAAC;YACrC,OAAO,CAAC,KAAK,CAAC,aAAa,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,4EAA4E;IAC5E,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CACX,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS;QAC5B,CAAC,CAAC,uFAAuF;QACzF,CAAC,CAAC,iCAAiC,MAAM,CAAC,IAAI,CAAC,OAAO,QAAQ,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CACnF,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FetchMessageObject } from 'imapflow';
|
|
2
|
+
import { type SecurityAssessment } from './analyze.js';
|
|
3
|
+
import { type AttachmentCandidate } from './attachments.js';
|
|
4
|
+
export interface MessageSummary {
|
|
5
|
+
uid: number;
|
|
6
|
+
subject: string;
|
|
7
|
+
from: string;
|
|
8
|
+
to: string;
|
|
9
|
+
date: string | undefined;
|
|
10
|
+
size: number | undefined;
|
|
11
|
+
flags: string[];
|
|
12
|
+
seen: boolean;
|
|
13
|
+
flagged: boolean;
|
|
14
|
+
answered: boolean;
|
|
15
|
+
hasAttachments: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Projection of an envelope fetch. Every string here came from the sender. */
|
|
18
|
+
export declare function summarize(message: FetchMessageObject): MessageSummary;
|
|
19
|
+
export interface RenderedMessage {
|
|
20
|
+
/** Server-side facts and verdicts. Safe to present as this server's voice. */
|
|
21
|
+
metadata: {
|
|
22
|
+
uid: number;
|
|
23
|
+
date: string | undefined;
|
|
24
|
+
messageId: string | undefined;
|
|
25
|
+
/** The References/In-Reply-To chain, for reconstructing the conversation. */
|
|
26
|
+
references: string[];
|
|
27
|
+
security: SecurityAssessment;
|
|
28
|
+
attachments: Array<Pick<AttachmentCandidate, 'partId' | 'filename' | 'contentType' | 'size'>>;
|
|
29
|
+
};
|
|
30
|
+
/** Headers and body, written by the sender. */
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses a raw message and splits it into what this server knows and what the
|
|
35
|
+
* sender wrote.
|
|
36
|
+
*
|
|
37
|
+
* The split is the whole point: the metadata block can be trusted because the
|
|
38
|
+
* server produced it, and everything in `content` gets fenced by the caller so
|
|
39
|
+
* the model can tell where the server stops speaking.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderMessage(uid: number, source: Buffer, trustedAuthservId?: string): Promise<RenderedMessage>;
|
|
42
|
+
/**
|
|
43
|
+
* Message-IDs from the References/In-Reply-To chain, for thread reconstruction.
|
|
44
|
+
* Bounded: a long-running thread accumulates hundreds of them, and each one
|
|
45
|
+
* becomes a search term.
|
|
46
|
+
*/
|
|
47
|
+
export declare function threadIdsOf(parsed: {
|
|
48
|
+
messageId?: string | undefined;
|
|
49
|
+
references?: string | string[] | undefined;
|
|
50
|
+
inReplyTo?: string | undefined;
|
|
51
|
+
}): string[];
|
package/dist/message.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { simpleParser } from 'mailparser';
|
|
2
|
+
import { assess, htmlToText, sanitizeText, } from './analyze.js';
|
|
3
|
+
import { collectAttachments } from './attachments.js';
|
|
4
|
+
/**
|
|
5
|
+
* Per-field caps. One oversized header must not be able to eat the whole result
|
|
6
|
+
* budget on its own — a 2 MB Subject is legal MIME.
|
|
7
|
+
*/
|
|
8
|
+
const SUBJECT_MAX = 2000;
|
|
9
|
+
const ADDRESS_MAX = 4000;
|
|
10
|
+
/** RFC 5322 allows a long Message-ID; nothing needs more than this to be useful. */
|
|
11
|
+
const MESSAGE_ID_MAX = 256;
|
|
12
|
+
/** Projection of an envelope fetch. Every string here came from the sender. */
|
|
13
|
+
export function summarize(message) {
|
|
14
|
+
const envelope = message.envelope;
|
|
15
|
+
const flags = [...(message.flags ?? [])];
|
|
16
|
+
return {
|
|
17
|
+
uid: message.uid,
|
|
18
|
+
subject: sanitizeText(envelope?.subject ?? '(no subject)', SUBJECT_MAX),
|
|
19
|
+
from: sanitizeText(formatEnvelopeAddresses(envelope?.from), ADDRESS_MAX),
|
|
20
|
+
to: sanitizeText(formatEnvelopeAddresses(envelope?.to), ADDRESS_MAX),
|
|
21
|
+
date: isoDate(envelope?.date ?? message.internalDate),
|
|
22
|
+
size: message.size,
|
|
23
|
+
flags,
|
|
24
|
+
seen: flags.includes('\\Seen'),
|
|
25
|
+
flagged: flags.includes('\\Flagged'),
|
|
26
|
+
answered: flags.includes('\\Answered'),
|
|
27
|
+
hasAttachments: collectAttachments(message.bodyStructure).length > 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** imapflow hands back a Date, but a malformed header can leave a raw string. */
|
|
31
|
+
function isoDate(value) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
35
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
36
|
+
}
|
|
37
|
+
function formatEnvelopeAddresses(addresses) {
|
|
38
|
+
if (addresses === undefined || addresses.length === 0)
|
|
39
|
+
return '(none)';
|
|
40
|
+
return addresses
|
|
41
|
+
.map((entry) => {
|
|
42
|
+
const address = entry.address ?? '(no address)';
|
|
43
|
+
return entry.name === undefined || entry.name === ''
|
|
44
|
+
? address
|
|
45
|
+
: `${entry.name} <${address}>`;
|
|
46
|
+
})
|
|
47
|
+
.join(', ');
|
|
48
|
+
}
|
|
49
|
+
function formatParsedAddresses(value) {
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return '(none)';
|
|
52
|
+
const list = Array.isArray(value) ? value : [value];
|
|
53
|
+
const text = list.map((entry) => entry.text).join(', ');
|
|
54
|
+
return text === '' ? '(none)' : text;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parses a raw message and splits it into what this server knows and what the
|
|
58
|
+
* sender wrote.
|
|
59
|
+
*
|
|
60
|
+
* The split is the whole point: the metadata block can be trusted because the
|
|
61
|
+
* server produced it, and everything in `content` gets fenced by the caller so
|
|
62
|
+
* the model can tell where the server stops speaking.
|
|
63
|
+
*/
|
|
64
|
+
export async function renderMessage(uid, source, trustedAuthservId) {
|
|
65
|
+
const parsed = await simpleParser(source, {
|
|
66
|
+
// Attachments are fetched deliberately, one at a time, through the policy
|
|
67
|
+
// in attachments.ts. Parsing them here would pull every byte into memory
|
|
68
|
+
// for a call that only wants the text.
|
|
69
|
+
skipImageLinks: true,
|
|
70
|
+
});
|
|
71
|
+
const body = bodyTextOf(parsed);
|
|
72
|
+
const text = sanitizeText(body);
|
|
73
|
+
const security = assess(`${parsed.subject ?? ''}\n${text}`, headerValue(parsed, 'authentication-results'), trustedAuthservId);
|
|
74
|
+
const content = [
|
|
75
|
+
`From: ${sanitizeText(formatParsedAddresses(parsed.from), ADDRESS_MAX)}`,
|
|
76
|
+
`To: ${sanitizeText(formatParsedAddresses(parsed.to), ADDRESS_MAX)}`,
|
|
77
|
+
parsed.cc === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: `Cc: ${sanitizeText(formatParsedAddresses(parsed.cc), ADDRESS_MAX)}`,
|
|
80
|
+
`Subject: ${sanitizeText(parsed.subject ?? '(no subject)', SUBJECT_MAX)}`,
|
|
81
|
+
'',
|
|
82
|
+
text,
|
|
83
|
+
]
|
|
84
|
+
.filter((line) => line !== undefined)
|
|
85
|
+
.join('\n');
|
|
86
|
+
return {
|
|
87
|
+
metadata: {
|
|
88
|
+
uid,
|
|
89
|
+
date: parsed.date?.toISOString(),
|
|
90
|
+
// Sender-chosen, unbounded in length, and it lands in the metadata block
|
|
91
|
+
// *outside* the fence — the one part of the result the model is told is
|
|
92
|
+
// ours. Every other sender string on this path is sanitised; this one
|
|
93
|
+
// was going through raw.
|
|
94
|
+
messageId: parsed.messageId === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: sanitizeText(parsed.messageId, MESSAGE_ID_MAX),
|
|
97
|
+
references: threadIdsOf({
|
|
98
|
+
...(parsed.references === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { references: parsed.references }),
|
|
101
|
+
...(parsed.inReplyTo === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { inReplyTo: parsed.inReplyTo }),
|
|
104
|
+
}),
|
|
105
|
+
security,
|
|
106
|
+
attachments: [],
|
|
107
|
+
},
|
|
108
|
+
content,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Prefers the plain-text part, falls back to converting the HTML one.
|
|
113
|
+
*
|
|
114
|
+
* mailparser's own `text` fallback is deliberately not used: it keeps content
|
|
115
|
+
* the recipient never sees, which is precisely where an instruction meant only
|
|
116
|
+
* for the model would be parked.
|
|
117
|
+
*/
|
|
118
|
+
function bodyTextOf(parsed) {
|
|
119
|
+
if (typeof parsed.text === 'string' && parsed.text.trim() !== '') {
|
|
120
|
+
return parsed.text;
|
|
121
|
+
}
|
|
122
|
+
if (typeof parsed.html === 'string' && parsed.html !== '') {
|
|
123
|
+
return htmlToText(parsed.html);
|
|
124
|
+
}
|
|
125
|
+
return '(no text content)';
|
|
126
|
+
}
|
|
127
|
+
function headerValue(parsed, name) {
|
|
128
|
+
const value = parsed.headers.get(name);
|
|
129
|
+
if (typeof value === 'string')
|
|
130
|
+
return value;
|
|
131
|
+
if (Array.isArray(value))
|
|
132
|
+
return value.filter((v) => typeof v === 'string').join('\n');
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Message-IDs from the References/In-Reply-To chain, for thread reconstruction.
|
|
137
|
+
* Bounded: a long-running thread accumulates hundreds of them, and each one
|
|
138
|
+
* becomes a search term.
|
|
139
|
+
*/
|
|
140
|
+
export function threadIdsOf(parsed) {
|
|
141
|
+
const references = parsed.references === undefined
|
|
142
|
+
? []
|
|
143
|
+
: Array.isArray(parsed.references)
|
|
144
|
+
? parsed.references
|
|
145
|
+
: parsed.references.split(/\s+/);
|
|
146
|
+
const all = [
|
|
147
|
+
...references,
|
|
148
|
+
...(parsed.inReplyTo === undefined ? [] : [parsed.inReplyTo]),
|
|
149
|
+
...(parsed.messageId === undefined ? [] : [parsed.messageId]),
|
|
150
|
+
]
|
|
151
|
+
.map((id) => id.trim())
|
|
152
|
+
.filter((id) => /^<[^\s<>]{1,255}>$/.test(id));
|
|
153
|
+
return [...new Set(all)].slice(0, 50);
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=message.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message.js","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { Config } from './config.js';
|
|
3
|
+
import { ImapClient } from './imap.js';
|
|
4
|
+
/**
|
|
5
|
+
* Exposes attachments as MCP resources.
|
|
6
|
+
*
|
|
7
|
+
* This is the second half of "downloadable": where `IMAP_DOWNLOAD_DIR` is unset
|
|
8
|
+
* — a container, a remote deployment, anywhere the server has no useful
|
|
9
|
+
* filesystem — the client can still fetch the bytes itself over the protocol
|
|
10
|
+
* instead of having them base64-encoded into the conversation.
|
|
11
|
+
*
|
|
12
|
+
* The read callback re-runs the *entire* attachment policy. It has to: a
|
|
13
|
+
* resource read does not go through `get_attachments`, so anything enforced
|
|
14
|
+
* only there would simply be a second, unguarded door to the same bytes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerAttachmentResources(server: McpServer, client: ImapClient, config: Config): void;
|