@gnidreve/classic-imap-smtp-mcp 0.3.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 +341 -0
- package/dist/main.js +2924 -0
- package/dist/main.js.map +1 -0
- package/llms.txt +262 -0
- package/package.json +71 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2924 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/main.ts
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync as writeFileSync5 } from "fs";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
|
|
7
|
+
// src/config/loader.ts
|
|
8
|
+
import { readFileSync, statSync } from "fs";
|
|
9
|
+
import { parse as parseToml } from "smol-toml";
|
|
10
|
+
|
|
11
|
+
// src/lib/errors.ts
|
|
12
|
+
var McpMailError = class extends Error {
|
|
13
|
+
constructor(code, message, details, imapResponse) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.details = details;
|
|
17
|
+
this.imapResponse = imapResponse;
|
|
18
|
+
this.name = new.target.name;
|
|
19
|
+
}
|
|
20
|
+
code;
|
|
21
|
+
details;
|
|
22
|
+
imapResponse;
|
|
23
|
+
toResult() {
|
|
24
|
+
return {
|
|
25
|
+
code: this.code,
|
|
26
|
+
message: this.message,
|
|
27
|
+
...this.details ? { details: this.details } : {},
|
|
28
|
+
...this.imapResponse ? { imap_response: this.imapResponse } : {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var AuthError = class extends McpMailError {
|
|
33
|
+
constructor(message = "Authentication failed", details) {
|
|
34
|
+
super("AUTH_FAILED", message, details);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var MailboxNotFoundError = class extends McpMailError {
|
|
38
|
+
constructor(mailbox) {
|
|
39
|
+
super("MAILBOX_NOT_FOUND", `Mailbox not found: ${mailbox}`, { mailbox });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var UidNotFoundError = class extends McpMailError {
|
|
43
|
+
constructor(uid, mailbox) {
|
|
44
|
+
super("UID_NOT_FOUND", `UID ${uid} not found in ${mailbox}`, { uid, mailbox });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var AttachmentNotFoundError = class extends McpMailError {
|
|
48
|
+
constructor(ref) {
|
|
49
|
+
super("ATTACHMENT_NOT_FOUND", `Attachment not found: ${ref}`, { ref });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var AccountNotFoundError = class extends McpMailError {
|
|
53
|
+
constructor(account) {
|
|
54
|
+
super("ACCOUNT_NOT_FOUND", `Account not configured: ${account}`, { account });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var RateLimitError = class extends McpMailError {
|
|
58
|
+
constructor(message = "Rate limit exceeded") {
|
|
59
|
+
super("RATE_LIMITED", message);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var TlsError = class extends McpMailError {
|
|
63
|
+
constructor(message, details) {
|
|
64
|
+
super("TLS_ERROR", message, details);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var ConfigError = class extends McpMailError {
|
|
68
|
+
constructor(message, details) {
|
|
69
|
+
super("CONFIG_ERROR", message, details);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var PermissionError = class extends McpMailError {
|
|
73
|
+
constructor(message, details) {
|
|
74
|
+
super("PERMISSION_DENIED", message, details);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var ImapProtocolError = class extends McpMailError {
|
|
78
|
+
constructor(message, imapResponse) {
|
|
79
|
+
super("IMAP_PROTOCOL_ERROR", message, void 0, imapResponse);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var SmtpRelayError = class extends McpMailError {
|
|
83
|
+
constructor(message, details) {
|
|
84
|
+
super("SMTP_RELAY_ERROR", message, details);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/config/providers.ts
|
|
89
|
+
var PROVIDERS = {
|
|
90
|
+
"gmail.com": {
|
|
91
|
+
imap_host: "imap.gmail.com",
|
|
92
|
+
imap_port: 993,
|
|
93
|
+
imap_tls: "implicit",
|
|
94
|
+
smtp_host: "smtp.gmail.com",
|
|
95
|
+
smtp_port: 465,
|
|
96
|
+
smtp_tls: "implicit"
|
|
97
|
+
},
|
|
98
|
+
"outlook.com": {
|
|
99
|
+
imap_host: "outlook.office365.com",
|
|
100
|
+
imap_port: 993,
|
|
101
|
+
imap_tls: "implicit",
|
|
102
|
+
smtp_host: "smtp.office365.com",
|
|
103
|
+
smtp_port: 587,
|
|
104
|
+
smtp_tls: "starttls"
|
|
105
|
+
},
|
|
106
|
+
"hotmail.com": {
|
|
107
|
+
imap_host: "outlook.office365.com",
|
|
108
|
+
imap_port: 993,
|
|
109
|
+
imap_tls: "implicit",
|
|
110
|
+
smtp_host: "smtp.office365.com",
|
|
111
|
+
smtp_port: 587,
|
|
112
|
+
smtp_tls: "starttls"
|
|
113
|
+
},
|
|
114
|
+
"live.com": {
|
|
115
|
+
imap_host: "outlook.office365.com",
|
|
116
|
+
imap_port: 993,
|
|
117
|
+
imap_tls: "implicit",
|
|
118
|
+
smtp_host: "smtp.office365.com",
|
|
119
|
+
smtp_port: 587,
|
|
120
|
+
smtp_tls: "starttls"
|
|
121
|
+
},
|
|
122
|
+
"icloud.com": {
|
|
123
|
+
imap_host: "imap.mail.me.com",
|
|
124
|
+
imap_port: 993,
|
|
125
|
+
imap_tls: "implicit",
|
|
126
|
+
smtp_host: "smtp.mail.me.com",
|
|
127
|
+
smtp_port: 587,
|
|
128
|
+
smtp_tls: "starttls"
|
|
129
|
+
},
|
|
130
|
+
"me.com": {
|
|
131
|
+
imap_host: "imap.mail.me.com",
|
|
132
|
+
imap_port: 993,
|
|
133
|
+
imap_tls: "implicit",
|
|
134
|
+
smtp_host: "smtp.mail.me.com",
|
|
135
|
+
smtp_port: 587,
|
|
136
|
+
smtp_tls: "starttls"
|
|
137
|
+
},
|
|
138
|
+
"yahoo.com": {
|
|
139
|
+
imap_host: "imap.mail.yahoo.com",
|
|
140
|
+
imap_port: 993,
|
|
141
|
+
imap_tls: "implicit",
|
|
142
|
+
smtp_host: "smtp.mail.yahoo.com",
|
|
143
|
+
smtp_port: 465,
|
|
144
|
+
smtp_tls: "implicit"
|
|
145
|
+
},
|
|
146
|
+
"fastmail.com": {
|
|
147
|
+
imap_host: "imap.fastmail.com",
|
|
148
|
+
imap_port: 993,
|
|
149
|
+
imap_tls: "implicit",
|
|
150
|
+
smtp_host: "smtp.fastmail.com",
|
|
151
|
+
smtp_port: 465,
|
|
152
|
+
smtp_tls: "implicit"
|
|
153
|
+
},
|
|
154
|
+
"posteo.de": {
|
|
155
|
+
imap_host: "posteo.de",
|
|
156
|
+
imap_port: 993,
|
|
157
|
+
imap_tls: "implicit",
|
|
158
|
+
smtp_host: "posteo.de",
|
|
159
|
+
smtp_port: 465,
|
|
160
|
+
smtp_tls: "implicit"
|
|
161
|
+
},
|
|
162
|
+
"mailbox.org": {
|
|
163
|
+
imap_host: "imap.mailbox.org",
|
|
164
|
+
imap_port: 993,
|
|
165
|
+
imap_tls: "implicit",
|
|
166
|
+
smtp_host: "smtp.mailbox.org",
|
|
167
|
+
smtp_port: 465,
|
|
168
|
+
smtp_tls: "implicit"
|
|
169
|
+
},
|
|
170
|
+
"gmx.net": {
|
|
171
|
+
imap_host: "imap.gmx.net",
|
|
172
|
+
imap_port: 993,
|
|
173
|
+
imap_tls: "implicit",
|
|
174
|
+
smtp_host: "mail.gmx.net",
|
|
175
|
+
smtp_port: 465,
|
|
176
|
+
smtp_tls: "implicit"
|
|
177
|
+
},
|
|
178
|
+
"gmx.de": {
|
|
179
|
+
imap_host: "imap.gmx.net",
|
|
180
|
+
imap_port: 993,
|
|
181
|
+
imap_tls: "implicit",
|
|
182
|
+
smtp_host: "mail.gmx.net",
|
|
183
|
+
smtp_port: 465,
|
|
184
|
+
smtp_tls: "implicit"
|
|
185
|
+
},
|
|
186
|
+
"web.de": {
|
|
187
|
+
imap_host: "imap.web.de",
|
|
188
|
+
imap_port: 993,
|
|
189
|
+
imap_tls: "implicit",
|
|
190
|
+
smtp_host: "smtp.web.de",
|
|
191
|
+
smtp_port: 587,
|
|
192
|
+
smtp_tls: "starttls"
|
|
193
|
+
},
|
|
194
|
+
"proton.me": {
|
|
195
|
+
imap_host: "127.0.0.1",
|
|
196
|
+
imap_port: 1143,
|
|
197
|
+
imap_tls: "starttls",
|
|
198
|
+
smtp_host: "127.0.0.1",
|
|
199
|
+
smtp_port: 1025,
|
|
200
|
+
smtp_tls: "starttls"
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function detectProvider(email) {
|
|
204
|
+
const domain = email.split("@")[1]?.toLowerCase();
|
|
205
|
+
return domain ? PROVIDERS[domain] : void 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/config/schema.ts
|
|
209
|
+
import { z } from "zod";
|
|
210
|
+
var tlsModeSchema = z.union([
|
|
211
|
+
z.literal("implicit"),
|
|
212
|
+
z.literal("starttls"),
|
|
213
|
+
z.literal("none")
|
|
214
|
+
]);
|
|
215
|
+
var accountSchema = z.object({
|
|
216
|
+
name: z.string().min(1),
|
|
217
|
+
user: z.string().min(1),
|
|
218
|
+
pass: z.string().min(1),
|
|
219
|
+
from_name: z.string().optional(),
|
|
220
|
+
imap_host: z.string().optional(),
|
|
221
|
+
// optional bei Auto-Detect-Providern
|
|
222
|
+
imap_port: z.number().int().positive().default(993),
|
|
223
|
+
imap_tls: tlsModeSchema.default("implicit"),
|
|
224
|
+
smtp_host: z.string().optional(),
|
|
225
|
+
smtp_port: z.number().int().positive().default(465),
|
|
226
|
+
smtp_tls: tlsModeSchema.default("implicit"),
|
|
227
|
+
verify_tls: z.boolean().default(true)
|
|
228
|
+
});
|
|
229
|
+
var limitsSchema = z.object({
|
|
230
|
+
smtp_per_minute: z.number().int().positive().default(10),
|
|
231
|
+
imap_ops_per_second: z.number().int().positive().default(100)
|
|
232
|
+
});
|
|
233
|
+
var fileConfigSchema = z.object({
|
|
234
|
+
default_account: z.string().optional(),
|
|
235
|
+
limits: limitsSchema.default({ smtp_per_minute: 10, imap_ops_per_second: 100 }),
|
|
236
|
+
accounts: z.array(accountSchema).min(1)
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// src/config/xdg.ts
|
|
240
|
+
import { homedir, platform } from "os";
|
|
241
|
+
import { join } from "path";
|
|
242
|
+
var APP = "classic-imap-smtp-mcp";
|
|
243
|
+
function defaultConfigPath() {
|
|
244
|
+
if (platform() === "win32") {
|
|
245
|
+
const appData = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
246
|
+
return join(appData, APP, "config.toml");
|
|
247
|
+
}
|
|
248
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
249
|
+
return join(xdg, APP, "config.toml");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/config/loader.ts
|
|
253
|
+
var ENV = "CLASSIC_IMAP_SMTP_";
|
|
254
|
+
function loadFromEnv() {
|
|
255
|
+
const user = process.env[`${ENV}USER`];
|
|
256
|
+
const pass = process.env[`${ENV}PASS`];
|
|
257
|
+
if (!user || !pass) return null;
|
|
258
|
+
const preset = detectProvider(user);
|
|
259
|
+
const raw = {
|
|
260
|
+
name: "default",
|
|
261
|
+
user,
|
|
262
|
+
pass,
|
|
263
|
+
from_name: process.env[`${ENV}FROM_NAME`],
|
|
264
|
+
imap_host: process.env[`${ENV}IMAP_HOST`] ?? preset?.imap_host,
|
|
265
|
+
imap_port: numEnv(`${ENV}IMAP_PORT`) ?? preset?.imap_port ?? 993,
|
|
266
|
+
imap_tls: process.env[`${ENV}IMAP_TLS`] ?? preset?.imap_tls ?? "implicit",
|
|
267
|
+
smtp_host: process.env[`${ENV}SMTP_HOST`] ?? preset?.smtp_host,
|
|
268
|
+
smtp_port: numEnv(`${ENV}SMTP_PORT`) ?? preset?.smtp_port ?? 465,
|
|
269
|
+
smtp_tls: process.env[`${ENV}SMTP_TLS`] ?? preset?.smtp_tls ?? "implicit",
|
|
270
|
+
verify_tls: process.env[`${ENV}VERIFY_TLS`] !== "false"
|
|
271
|
+
};
|
|
272
|
+
const account = accountSchema.parse(normalizeTls(raw));
|
|
273
|
+
return { mode: "env", defaultAccount: "default", accounts: /* @__PURE__ */ new Map([["default", account]]) };
|
|
274
|
+
}
|
|
275
|
+
function loadFromFile(path) {
|
|
276
|
+
let text;
|
|
277
|
+
try {
|
|
278
|
+
checkPermissions(path);
|
|
279
|
+
text = readFileSync(path, "utf8");
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (err instanceof PermissionError) throw err;
|
|
282
|
+
throw new ConfigError(`Cannot read config file: ${path}`, { cause: String(err) });
|
|
283
|
+
}
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
parsed = fileConfigSchema.parse(parseToml(text));
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new ConfigError("Invalid config file", { cause: String(err) });
|
|
289
|
+
}
|
|
290
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
291
|
+
for (const acc of parsed.accounts) {
|
|
292
|
+
const preset = detectProvider(acc.user);
|
|
293
|
+
accounts.set(acc.name, {
|
|
294
|
+
...acc,
|
|
295
|
+
imap_host: acc.imap_host ?? preset?.imap_host,
|
|
296
|
+
smtp_host: acc.smtp_host ?? preset?.smtp_host
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const defaultAccount = parsed.default_account ?? parsed.accounts[0].name;
|
|
300
|
+
return { mode: "config-file", defaultAccount, accounts, configPath: path };
|
|
301
|
+
}
|
|
302
|
+
function loadConfig(explicitPath) {
|
|
303
|
+
const fromEnv = loadFromEnv();
|
|
304
|
+
if (fromEnv) return fromEnv;
|
|
305
|
+
const path = explicitPath ?? defaultConfigPath();
|
|
306
|
+
return loadFromFile(path);
|
|
307
|
+
}
|
|
308
|
+
function checkPermissions(path) {
|
|
309
|
+
if (process.platform === "win32") return;
|
|
310
|
+
const mode = statSync(path).mode & 511;
|
|
311
|
+
if (mode & 63) {
|
|
312
|
+
throw new PermissionError(
|
|
313
|
+
`Config file ${path} is too permissive (mode ${mode.toString(8)}); expected 0600`,
|
|
314
|
+
{ path, mode: mode.toString(8) }
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function numEnv(key) {
|
|
319
|
+
const v = process.env[key];
|
|
320
|
+
return v ? Number.parseInt(v, 10) : void 0;
|
|
321
|
+
}
|
|
322
|
+
function normalizeTls(raw) {
|
|
323
|
+
const map = (v) => v === "true" || v === true ? "implicit" : v === "false" || v === false ? "none" : v;
|
|
324
|
+
return { ...raw, imap_tls: map(raw.imap_tls), smtp_tls: map(raw.smtp_tls) };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/connections/imap-pool.ts
|
|
328
|
+
import { ImapFlow } from "imapflow";
|
|
329
|
+
var MAX_RETRIES = 5;
|
|
330
|
+
var BASE_DELAY_MS = 1e3;
|
|
331
|
+
var MAX_DELAY_MS = 6e4;
|
|
332
|
+
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
333
|
+
var ImapPool = class {
|
|
334
|
+
constructor(config, logger) {
|
|
335
|
+
this.config = config;
|
|
336
|
+
this.logger = logger;
|
|
337
|
+
}
|
|
338
|
+
config;
|
|
339
|
+
logger;
|
|
340
|
+
connections = /* @__PURE__ */ new Map();
|
|
341
|
+
async acquire(account) {
|
|
342
|
+
const existing = this.connections.get(account);
|
|
343
|
+
if (existing?.client.usable) {
|
|
344
|
+
existing.lastUsed = Date.now();
|
|
345
|
+
return existing.client;
|
|
346
|
+
}
|
|
347
|
+
if (existing) {
|
|
348
|
+
try {
|
|
349
|
+
await existing.client.logout();
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
this.connections.delete(account);
|
|
353
|
+
}
|
|
354
|
+
const accConfig = this.config.accounts.get(account);
|
|
355
|
+
if (!accConfig) {
|
|
356
|
+
throw new ImapProtocolError(`Account ${account} not configured`);
|
|
357
|
+
}
|
|
358
|
+
const client = await this.connectWithRetry(accConfig);
|
|
359
|
+
this.connections.set(account, { client, lastUsed: Date.now() });
|
|
360
|
+
return client;
|
|
361
|
+
}
|
|
362
|
+
async connectWithRetry(acc, attempt = 0) {
|
|
363
|
+
try {
|
|
364
|
+
const opts = {
|
|
365
|
+
// biome-ignore lint/style/noNonNullAssertion: config validated at load
|
|
366
|
+
host: acc.imap_host,
|
|
367
|
+
port: acc.imap_port,
|
|
368
|
+
auth: {
|
|
369
|
+
user: acc.user,
|
|
370
|
+
pass: acc.pass
|
|
371
|
+
},
|
|
372
|
+
logger: false,
|
|
373
|
+
secure: acc.imap_tls !== "starttls" && acc.imap_tls !== "none"
|
|
374
|
+
};
|
|
375
|
+
if (acc.imap_tls === "none") {
|
|
376
|
+
opts.disableAutoIdle = true;
|
|
377
|
+
}
|
|
378
|
+
if (!acc.verify_tls) {
|
|
379
|
+
opts.tls = { rejectUnauthorized: false };
|
|
380
|
+
}
|
|
381
|
+
const client = new ImapFlow(opts);
|
|
382
|
+
await client.connect();
|
|
383
|
+
this.logger.info({ account: acc.name }, "IMAP connected");
|
|
384
|
+
return client;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
const error = err;
|
|
387
|
+
if (error.message?.includes("TLS") || error.message?.includes("certificate")) {
|
|
388
|
+
throw new TlsError(`IMAP TLS error: ${error.message}`);
|
|
389
|
+
}
|
|
390
|
+
if (error.message?.includes("authentication") || error.message?.includes("login") || error.message?.includes("Auth")) {
|
|
391
|
+
throw new AuthError(`IMAP authentication failed: ${error.message}`);
|
|
392
|
+
}
|
|
393
|
+
if (attempt < MAX_RETRIES) {
|
|
394
|
+
const delay = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
|
|
395
|
+
this.logger.warn({ account: acc.name, attempt: attempt + 1, delay }, "IMAP reconnect");
|
|
396
|
+
await sleep(delay);
|
|
397
|
+
return this.connectWithRetry(acc, attempt + 1);
|
|
398
|
+
}
|
|
399
|
+
throw new ImapProtocolError(`Failed to connect to IMAP: ${error.message}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async pruneIdle() {
|
|
403
|
+
const now = Date.now();
|
|
404
|
+
for (const [account, entry] of this.connections) {
|
|
405
|
+
if (now - entry.lastUsed > IDLE_TIMEOUT_MS) {
|
|
406
|
+
this.logger.info({ account }, "Closing idle IMAP connection");
|
|
407
|
+
try {
|
|
408
|
+
await entry.client.logout();
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
this.connections.delete(account);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async closeAll() {
|
|
416
|
+
for (const [account, entry] of this.connections) {
|
|
417
|
+
try {
|
|
418
|
+
if (entry.client.usable) await entry.client.logout();
|
|
419
|
+
} catch (err) {
|
|
420
|
+
this.logger.warn({ account, err }, "Error closing IMAP connection");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
this.connections.clear();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
function sleep(ms) {
|
|
427
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/connections/smtp-pool.ts
|
|
431
|
+
import nodemailer from "nodemailer";
|
|
432
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
433
|
+
var DEFAULT_RATE_LIMIT = 10;
|
|
434
|
+
var SmtpPool = class {
|
|
435
|
+
constructor(config, logger) {
|
|
436
|
+
this.config = config;
|
|
437
|
+
this.logger = logger;
|
|
438
|
+
}
|
|
439
|
+
config;
|
|
440
|
+
logger;
|
|
441
|
+
transports = /* @__PURE__ */ new Map();
|
|
442
|
+
rateBuckets = /* @__PURE__ */ new Map();
|
|
443
|
+
// Liefert einen nodemailer-Transporter für den Account.
|
|
444
|
+
async acquire(account) {
|
|
445
|
+
this.checkRateLimit(account);
|
|
446
|
+
const existing = this.transports.get(account);
|
|
447
|
+
if (existing) return existing;
|
|
448
|
+
const accConfig = this.config.accounts.get(account);
|
|
449
|
+
if (!accConfig) {
|
|
450
|
+
throw new SmtpRelayError(`Account ${account} not configured`);
|
|
451
|
+
}
|
|
452
|
+
const transport = await this.createTransport(accConfig);
|
|
453
|
+
this.transports.set(account, transport);
|
|
454
|
+
return transport;
|
|
455
|
+
}
|
|
456
|
+
checkRateLimit(account) {
|
|
457
|
+
const limit = DEFAULT_RATE_LIMIT;
|
|
458
|
+
const bucket = this.rateBuckets.get(account) ?? { tokens: limit, lastRefill: Date.now() };
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
const elapsed = now - bucket.lastRefill;
|
|
461
|
+
if (elapsed >= RATE_LIMIT_WINDOW_MS) {
|
|
462
|
+
bucket.tokens = limit;
|
|
463
|
+
bucket.lastRefill = now;
|
|
464
|
+
}
|
|
465
|
+
if (bucket.tokens <= 0) {
|
|
466
|
+
throw new RateLimitError(`SMTP rate limit (${limit}/minute) reached for account ${account}`);
|
|
467
|
+
}
|
|
468
|
+
bucket.tokens--;
|
|
469
|
+
this.rateBuckets.set(account, bucket);
|
|
470
|
+
}
|
|
471
|
+
async createTransport(acc) {
|
|
472
|
+
try {
|
|
473
|
+
const tlsOpts = {};
|
|
474
|
+
if (!acc.verify_tls) {
|
|
475
|
+
tlsOpts.rejectUnauthorized = false;
|
|
476
|
+
}
|
|
477
|
+
const transporter = nodemailer.createTransport({
|
|
478
|
+
// biome-ignore lint/style/noNonNullAssertion: config validated at load
|
|
479
|
+
host: acc.smtp_host,
|
|
480
|
+
port: acc.smtp_port,
|
|
481
|
+
secure: acc.smtp_tls === "implicit",
|
|
482
|
+
auth: {
|
|
483
|
+
user: acc.user,
|
|
484
|
+
pass: acc.pass
|
|
485
|
+
},
|
|
486
|
+
tls: tlsOpts,
|
|
487
|
+
// STARTTLS: wird von nodemailer automatisch bei secure=false und Port 587/25 verwendet
|
|
488
|
+
pool: true,
|
|
489
|
+
maxConnections: 1,
|
|
490
|
+
maxMessages: 100,
|
|
491
|
+
logger: false
|
|
492
|
+
});
|
|
493
|
+
this.logger.info({ account: acc.name }, "SMTP transport created");
|
|
494
|
+
return transporter;
|
|
495
|
+
} catch (err) {
|
|
496
|
+
const error = err;
|
|
497
|
+
if (error.message?.includes("auth") || error.message?.includes("login")) {
|
|
498
|
+
throw new AuthError(`SMTP authentication failed: ${error.message}`);
|
|
499
|
+
}
|
|
500
|
+
throw new SmtpRelayError(`Failed to create SMTP transport: ${error.message}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async closeAll() {
|
|
504
|
+
for (const [account, transport] of this.transports) {
|
|
505
|
+
try {
|
|
506
|
+
transport.close();
|
|
507
|
+
} catch (err) {
|
|
508
|
+
this.logger.warn({ account, err }, "Error closing SMTP transport");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.transports.clear();
|
|
512
|
+
this.rateBuckets.clear();
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/server/logging.ts
|
|
517
|
+
import { pino } from "pino";
|
|
518
|
+
var REDACT_KEYS = ["pass", "password", "token", "secret", "apikey", "apiKey"];
|
|
519
|
+
function createLogger(opts = {}) {
|
|
520
|
+
const level = opts.level ?? "info";
|
|
521
|
+
const format = opts.format ?? "json";
|
|
522
|
+
if (format === "pretty") {
|
|
523
|
+
return pino({
|
|
524
|
+
level,
|
|
525
|
+
redact: { paths: REDACT_KEYS.flatMap((k) => [k, `*.${k}`, `*.*.${k}`]), censor: "***" },
|
|
526
|
+
transport: { target: "pino-pretty", options: { destination: 2 } }
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return pino({
|
|
530
|
+
level,
|
|
531
|
+
redact: { paths: REDACT_KEYS.flatMap((k) => [k, `*.${k}`, `*.*.${k}`]), censor: "***" }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/server/options.ts
|
|
536
|
+
function parseArgs(argv) {
|
|
537
|
+
const opts = {
|
|
538
|
+
safe: false,
|
|
539
|
+
readonly: false,
|
|
540
|
+
noImap: false,
|
|
541
|
+
noSmtp: false,
|
|
542
|
+
logLevel: "info",
|
|
543
|
+
logFormat: "json"
|
|
544
|
+
};
|
|
545
|
+
let subcommand;
|
|
546
|
+
let subcommandArg;
|
|
547
|
+
let help = false;
|
|
548
|
+
let version = false;
|
|
549
|
+
for (let i = 0; i < argv.length; i++) {
|
|
550
|
+
const arg = argv[i];
|
|
551
|
+
if (arg === "init" || arg === "test" || arg === "list-tools") {
|
|
552
|
+
subcommand = arg;
|
|
553
|
+
if (arg === "test" && argv[i + 1] && !argv[i + 1]?.startsWith("-")) {
|
|
554
|
+
subcommandArg = argv[++i];
|
|
555
|
+
}
|
|
556
|
+
} else if (arg === "--safe") opts.safe = true;
|
|
557
|
+
else if (arg === "--readonly") opts.readonly = true;
|
|
558
|
+
else if (arg === "--no-imap") opts.noImap = true;
|
|
559
|
+
else if (arg === "--no-smtp") opts.noSmtp = true;
|
|
560
|
+
else if (arg === "-h" || arg === "--help") help = true;
|
|
561
|
+
else if (arg === "-V" || arg === "--version") version = true;
|
|
562
|
+
else if (arg.startsWith("--allow-tools=")) opts.allowTools = csv(arg);
|
|
563
|
+
else if (arg.startsWith("--deny-tools=")) opts.denyTools = csv(arg);
|
|
564
|
+
else if (arg.startsWith("--account=")) opts.account = val(arg);
|
|
565
|
+
else if (arg.startsWith("--config=")) opts.configPath = val(arg);
|
|
566
|
+
else if (arg.startsWith("--log-level=")) opts.logLevel = val(arg);
|
|
567
|
+
else if (arg.startsWith("--log-format="))
|
|
568
|
+
opts.logFormat = val(arg) === "pretty" ? "pretty" : "json";
|
|
569
|
+
}
|
|
570
|
+
return { subcommand, subcommandArg, options: opts, help, version };
|
|
571
|
+
}
|
|
572
|
+
var val = (arg) => arg.slice(arg.indexOf("=") + 1);
|
|
573
|
+
var csv = (arg) => val(arg).split(",").map((s) => s.trim()).filter(Boolean);
|
|
574
|
+
|
|
575
|
+
// src/server/registry.ts
|
|
576
|
+
var READONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
577
|
+
"imap_list_mailboxes",
|
|
578
|
+
"imap_status_mailbox",
|
|
579
|
+
"imap_list_messages",
|
|
580
|
+
"imap_get_message",
|
|
581
|
+
"imap_get_message_headers",
|
|
582
|
+
"imap_get_message_raw",
|
|
583
|
+
"imap_get_messages_bulk",
|
|
584
|
+
"imap_search",
|
|
585
|
+
"imap_download_attachment",
|
|
586
|
+
"imap_get_thread",
|
|
587
|
+
"imap_get_quota",
|
|
588
|
+
"imap_check_capabilities",
|
|
589
|
+
"smtp_verify_connection",
|
|
590
|
+
"account_list",
|
|
591
|
+
"meta_health",
|
|
592
|
+
"meta_server_info"
|
|
593
|
+
]);
|
|
594
|
+
var DELETE_TOOLS = /* @__PURE__ */ new Set([
|
|
595
|
+
"imap_delete_message",
|
|
596
|
+
"imap_expunge",
|
|
597
|
+
"imap_delete_mailbox"
|
|
598
|
+
]);
|
|
599
|
+
function matchesAny(name, patterns) {
|
|
600
|
+
return patterns.some((p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p);
|
|
601
|
+
}
|
|
602
|
+
function passesFeatureFlags(t, o) {
|
|
603
|
+
if (o.noImap && t.category.startsWith("imap")) return false;
|
|
604
|
+
if (o.noSmtp && t.category === "smtp") return false;
|
|
605
|
+
if (o.readonly && !READONLY_TOOLS.has(t.name)) return false;
|
|
606
|
+
if (o.safe && DELETE_TOOLS.has(t.name)) return false;
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
function resolveActiveTools(all, o) {
|
|
610
|
+
const active = /* @__PURE__ */ new Set();
|
|
611
|
+
for (const t of all) if (passesFeatureFlags(t, o)) active.add(t.name);
|
|
612
|
+
if (o.allowTools?.length) {
|
|
613
|
+
for (const t of all) if (matchesAny(t.name, o.allowTools)) active.add(t.name);
|
|
614
|
+
}
|
|
615
|
+
if (o.denyTools?.length) {
|
|
616
|
+
for (const name of [...active]) if (matchesAny(name, o.denyTools)) active.delete(name);
|
|
617
|
+
}
|
|
618
|
+
return all.filter((t) => active.has(t.name));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/server/server.ts
|
|
622
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
623
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
624
|
+
|
|
625
|
+
// src/tools/imap-read/check-capabilities.ts
|
|
626
|
+
import { z as z2 } from "zod";
|
|
627
|
+
|
|
628
|
+
// src/tools/_types.ts
|
|
629
|
+
function defineTool(def) {
|
|
630
|
+
return def;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/tools/imap-read/check-capabilities.ts
|
|
634
|
+
var check_capabilities_default = defineTool({
|
|
635
|
+
name: "imap_check_capabilities",
|
|
636
|
+
description: "Server CAPABILITY-Liste",
|
|
637
|
+
category: "imap-read",
|
|
638
|
+
inputSchema: z2.object({
|
|
639
|
+
account: z2.string().optional().describe("Account name (default: default_account)")
|
|
640
|
+
}),
|
|
641
|
+
handler: async (input, ctx) => {
|
|
642
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
643
|
+
const client = await ctx.imap.acquire(accountName);
|
|
644
|
+
const caps = [...client.capabilities.keys()];
|
|
645
|
+
return { capabilities: caps };
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// src/tools/imap-read/download-attachment.ts
|
|
650
|
+
import { writeFileSync } from "fs";
|
|
651
|
+
import { z as z3 } from "zod";
|
|
652
|
+
var download_attachment_default = defineTool({
|
|
653
|
+
name: "imap_download_attachment",
|
|
654
|
+
description: "Gezielt eine MIME-Part extrahieren (Pfad oder Base64)",
|
|
655
|
+
category: "imap-read",
|
|
656
|
+
inputSchema: z3.object({
|
|
657
|
+
mailbox: z3.string().min(1).describe("Mailbox path"),
|
|
658
|
+
uid: z3.number().int().positive().describe("Message UID"),
|
|
659
|
+
partId: z3.string().min(1).describe("MIME part ID (e.g. '1', '2.1')"),
|
|
660
|
+
savePath: z3.string().optional().describe("Local path to save the attachment"),
|
|
661
|
+
account: z3.string().optional().describe("Account name (default: default_account)")
|
|
662
|
+
}),
|
|
663
|
+
handler: async (input, ctx) => {
|
|
664
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
665
|
+
const client = await ctx.imap.acquire(accountName);
|
|
666
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
667
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
668
|
+
const msg = await client.fetchOne(input.uid, {
|
|
669
|
+
uid: true,
|
|
670
|
+
bodyParts: [input.partId],
|
|
671
|
+
bodyStructure: true
|
|
672
|
+
});
|
|
673
|
+
if (!msg) throw new UidNotFoundError(input.uid, input.mailbox);
|
|
674
|
+
const part = msg.bodyParts?.get(input.partId);
|
|
675
|
+
if (!part) throw new AttachmentNotFoundError(`Part ${input.partId}`);
|
|
676
|
+
let contentType = "application/octet-stream";
|
|
677
|
+
let filename = `attachment-${input.partId}`;
|
|
678
|
+
if (msg.bodyStructure) {
|
|
679
|
+
contentType = msg.bodyStructure.type || contentType;
|
|
680
|
+
filename = msg.bodyStructure.parameters?.name || msg.bodyStructure.parameters?.filename || filename;
|
|
681
|
+
}
|
|
682
|
+
const buffer = Buffer.from(part);
|
|
683
|
+
const size = buffer.length;
|
|
684
|
+
const base64 = buffer.toString("base64");
|
|
685
|
+
if (input.savePath) {
|
|
686
|
+
writeFileSync(input.savePath, buffer);
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
partId: input.partId,
|
|
690
|
+
...filename ? { filename } : {},
|
|
691
|
+
contentType,
|
|
692
|
+
size,
|
|
693
|
+
...input.savePath ? { savedPath: input.savePath } : { base64 }
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// src/tools/imap-read/get-message-headers.ts
|
|
699
|
+
import { z as z4 } from "zod";
|
|
700
|
+
var get_message_headers_default = defineTool({
|
|
701
|
+
name: "imap_get_message_headers",
|
|
702
|
+
description: "Nur Header einer Mail",
|
|
703
|
+
category: "imap-read",
|
|
704
|
+
inputSchema: z4.object({
|
|
705
|
+
mailbox: z4.string().min(1).describe("Mailbox path"),
|
|
706
|
+
uid: z4.number().int().positive().describe("Message UID"),
|
|
707
|
+
account: z4.string().optional().describe("Account name (default: default_account)")
|
|
708
|
+
}),
|
|
709
|
+
handler: async (input, ctx) => {
|
|
710
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
711
|
+
const client = await ctx.imap.acquire(accountName);
|
|
712
|
+
const mailbox = await client.mailboxOpen(input.mailbox);
|
|
713
|
+
if (!mailbox) throw new MailboxNotFoundError(input.mailbox);
|
|
714
|
+
const msg = await client.fetchOne(input.uid, {
|
|
715
|
+
uid: true,
|
|
716
|
+
headers: true
|
|
717
|
+
});
|
|
718
|
+
if (!msg) throw new UidNotFoundError(input.uid, input.mailbox);
|
|
719
|
+
const rawHeaders = msg.headers?.toString() ?? "";
|
|
720
|
+
const headers = {};
|
|
721
|
+
const lines = rawHeaders.split("\r\n");
|
|
722
|
+
let currentKey = null;
|
|
723
|
+
let currentValue = null;
|
|
724
|
+
for (const line of lines) {
|
|
725
|
+
if ((line.startsWith(" ") || line.startsWith(" ")) && currentKey) {
|
|
726
|
+
if (currentValue) {
|
|
727
|
+
currentValue += ` ${line.trim()}`;
|
|
728
|
+
}
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (currentKey && currentValue) {
|
|
732
|
+
const existing = headers[currentKey];
|
|
733
|
+
if (existing) {
|
|
734
|
+
if (Array.isArray(existing)) {
|
|
735
|
+
existing.push(currentValue);
|
|
736
|
+
} else {
|
|
737
|
+
headers[currentKey] = [existing, currentValue];
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
headers[currentKey] = currentValue;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const colonIdx = line.indexOf(":");
|
|
744
|
+
if (colonIdx > 0) {
|
|
745
|
+
currentKey = line.slice(0, colonIdx).trim();
|
|
746
|
+
currentValue = line.slice(colonIdx + 1).trim();
|
|
747
|
+
} else {
|
|
748
|
+
currentKey = null;
|
|
749
|
+
currentValue = null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (currentKey && currentValue) {
|
|
753
|
+
const existing = headers[currentKey];
|
|
754
|
+
if (existing) {
|
|
755
|
+
if (Array.isArray(existing)) {
|
|
756
|
+
existing.push(currentValue);
|
|
757
|
+
} else {
|
|
758
|
+
headers[currentKey] = [existing, currentValue];
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
headers[currentKey] = currentValue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
uid: msg.uid,
|
|
766
|
+
headers
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// src/tools/imap-read/get-message-raw.ts
|
|
772
|
+
import { z as z5 } from "zod";
|
|
773
|
+
var get_message_raw_default = defineTool({
|
|
774
|
+
name: "imap_get_message_raw",
|
|
775
|
+
description: "RFC-822 raw source einer Mail",
|
|
776
|
+
category: "imap-read",
|
|
777
|
+
inputSchema: z5.object({
|
|
778
|
+
mailbox: z5.string().min(1).describe("Mailbox path"),
|
|
779
|
+
uid: z5.number().int().positive().describe("Message UID"),
|
|
780
|
+
account: z5.string().optional().describe("Account name (default: default_account)")
|
|
781
|
+
}),
|
|
782
|
+
handler: async (input, ctx) => {
|
|
783
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
784
|
+
const client = await ctx.imap.acquire(accountName);
|
|
785
|
+
const mailbox = await client.mailboxOpen(input.mailbox);
|
|
786
|
+
if (!mailbox) throw new MailboxNotFoundError(input.mailbox);
|
|
787
|
+
const msg = await client.fetchOne(input.uid, {
|
|
788
|
+
uid: true,
|
|
789
|
+
source: true
|
|
790
|
+
});
|
|
791
|
+
if (!msg) throw new UidNotFoundError(input.uid, input.mailbox);
|
|
792
|
+
return {
|
|
793
|
+
uid: msg.uid,
|
|
794
|
+
rfc822: msg.source?.toString() ?? ""
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// src/tools/imap-read/get-message.ts
|
|
800
|
+
import { z as z6 } from "zod";
|
|
801
|
+
|
|
802
|
+
// src/lib/mime.ts
|
|
803
|
+
import { simpleParser } from "mailparser";
|
|
804
|
+
async function parseMime(raw) {
|
|
805
|
+
const parsed = await simpleParser(raw);
|
|
806
|
+
const body = {};
|
|
807
|
+
if (parsed.text) body.text = parsed.text;
|
|
808
|
+
if (parsed.html) body.html = parsed.html;
|
|
809
|
+
const attachments = (parsed.attachments || []).map(
|
|
810
|
+
(a, idx) => {
|
|
811
|
+
const fn = a.filename || extractFilenameFromHeaders(a.headers);
|
|
812
|
+
return {
|
|
813
|
+
partId: a.related ? `related.${idx + 1}` : String(idx + 1),
|
|
814
|
+
filename: fn ?? `attachment_${idx + 1}`,
|
|
815
|
+
contentType: a.contentType || "application/octet-stream",
|
|
816
|
+
size: a.size || a.content.length || 0,
|
|
817
|
+
...a.contentId ? { contentId: a.contentId } : {},
|
|
818
|
+
...a.contentDisposition === "inline" ? { disposition: "inline" } : a.contentDisposition === "attachment" ? { disposition: "attachment" } : {}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
return { body, attachments };
|
|
823
|
+
}
|
|
824
|
+
function extractFilenameFromHeaders(headers) {
|
|
825
|
+
if (!headers) return void 0;
|
|
826
|
+
const cd = headers.get("content-disposition");
|
|
827
|
+
if (typeof cd === "string") {
|
|
828
|
+
const m = cd.match(/filename\*?=(?:[^']*'[^']*')?([^;\s]+)/i);
|
|
829
|
+
if (m) {
|
|
830
|
+
try {
|
|
831
|
+
const captured = m[1];
|
|
832
|
+
return captured ? decodeURIComponent(captured) : void 0;
|
|
833
|
+
} catch {
|
|
834
|
+
return m[1];
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const m2 = cd.match(/filename="?([^";]*)"?/i);
|
|
838
|
+
if (m2) return m2[1];
|
|
839
|
+
}
|
|
840
|
+
return void 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/tools/imap-read/get-message.ts
|
|
844
|
+
function addressArray(addr) {
|
|
845
|
+
if (!addr) return [];
|
|
846
|
+
const list = Array.isArray(addr) ? addr : [addr];
|
|
847
|
+
const result = [];
|
|
848
|
+
for (const entry of list) {
|
|
849
|
+
if (entry && typeof entry === "object") {
|
|
850
|
+
const e = entry;
|
|
851
|
+
const addrStr = e.address ?? "";
|
|
852
|
+
if (addrStr) {
|
|
853
|
+
result.push({ ...e.name ? { name: e.name } : {}, address: addrStr });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
var get_message_default = defineTool({
|
|
860
|
+
name: "imap_get_message",
|
|
861
|
+
description: "Vollst\xE4ndige Mail inkl. geparstem Body + Attachment-Metadaten",
|
|
862
|
+
category: "imap-read",
|
|
863
|
+
inputSchema: z6.object({
|
|
864
|
+
mailbox: z6.string().min(1).describe("Mailbox path"),
|
|
865
|
+
uid: z6.number().int().positive().describe("Message UID"),
|
|
866
|
+
account: z6.string().optional().describe("Account name (default: default_account)")
|
|
867
|
+
}),
|
|
868
|
+
handler: async (input, ctx) => {
|
|
869
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
870
|
+
const client = await ctx.imap.acquire(accountName);
|
|
871
|
+
const mailbox = await client.mailboxOpen(input.mailbox);
|
|
872
|
+
if (!mailbox) throw new MailboxNotFoundError(input.mailbox);
|
|
873
|
+
const msg = await client.fetchOne(input.uid, {
|
|
874
|
+
uid: true,
|
|
875
|
+
envelope: true,
|
|
876
|
+
flags: true,
|
|
877
|
+
size: true,
|
|
878
|
+
source: true
|
|
879
|
+
});
|
|
880
|
+
if (!msg) throw new UidNotFoundError(input.uid, input.mailbox);
|
|
881
|
+
const raw = msg.source?.toString() ?? "";
|
|
882
|
+
let parsed;
|
|
883
|
+
try {
|
|
884
|
+
parsed = await parseMime(raw);
|
|
885
|
+
} catch {
|
|
886
|
+
parsed = { body: {}, attachments: [] };
|
|
887
|
+
}
|
|
888
|
+
const enc = msg.envelope;
|
|
889
|
+
return {
|
|
890
|
+
envelope: {
|
|
891
|
+
uid: msg.uid,
|
|
892
|
+
subject: enc?.subject ?? "",
|
|
893
|
+
from: addressArray(enc?.from),
|
|
894
|
+
to: addressArray(enc?.to),
|
|
895
|
+
...enc?.cc?.length ? { cc: addressArray(enc.cc) } : {},
|
|
896
|
+
...enc?.bcc?.length ? { bcc: addressArray(enc.bcc) } : {},
|
|
897
|
+
...enc?.replyTo?.length ? { replyTo: addressArray(enc.replyTo) } : {},
|
|
898
|
+
date: (enc?.date ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
899
|
+
...enc?.messageId ? { messageId: enc.messageId } : {},
|
|
900
|
+
...enc?.inReplyTo ? { inReplyTo: enc.inReplyTo } : {},
|
|
901
|
+
flags: [...msg.flags ?? []],
|
|
902
|
+
size: msg.size ?? 0,
|
|
903
|
+
hasAttachments: parsed.attachments.length > 0
|
|
904
|
+
},
|
|
905
|
+
...parsed.body.text ? { text: parsed.body.text } : {},
|
|
906
|
+
...parsed.body.html ? { html: parsed.body.html } : {},
|
|
907
|
+
attachments: parsed.attachments
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// src/tools/imap-read/get-messages-bulk.ts
|
|
913
|
+
import { z as z7 } from "zod";
|
|
914
|
+
function addressArray2(addr) {
|
|
915
|
+
if (!addr) return [];
|
|
916
|
+
const list = Array.isArray(addr) ? addr : [addr];
|
|
917
|
+
const result = [];
|
|
918
|
+
for (const entry of list) {
|
|
919
|
+
if (entry && typeof entry === "object") {
|
|
920
|
+
const e = entry;
|
|
921
|
+
const addrStr = e.address ?? "";
|
|
922
|
+
if (addrStr) {
|
|
923
|
+
result.push({ ...e.name ? { name: e.name } : {}, address: addrStr });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
var get_messages_bulk_default = defineTool({
|
|
930
|
+
name: "imap_get_messages_bulk",
|
|
931
|
+
description: "Bis N UIDs in einem Call holen",
|
|
932
|
+
category: "imap-read",
|
|
933
|
+
inputSchema: z7.object({
|
|
934
|
+
mailbox: z7.string().min(1).describe("Mailbox path"),
|
|
935
|
+
uids: z7.array(z7.number().int().positive()).min(1).max(500).describe("Message UIDs"),
|
|
936
|
+
account: z7.string().optional().describe("Account name (default: default_account)")
|
|
937
|
+
}),
|
|
938
|
+
handler: async (input, ctx) => {
|
|
939
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
940
|
+
const client = await ctx.imap.acquire(accountName);
|
|
941
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
942
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
943
|
+
const messages = [];
|
|
944
|
+
const notFound = [];
|
|
945
|
+
const uidSet = new Set(input.uids);
|
|
946
|
+
for await (const msg of client.fetch(input.uids, {
|
|
947
|
+
uid: true,
|
|
948
|
+
envelope: true,
|
|
949
|
+
flags: true,
|
|
950
|
+
size: true,
|
|
951
|
+
source: true
|
|
952
|
+
})) {
|
|
953
|
+
uidSet.delete(msg.uid);
|
|
954
|
+
const raw = msg.source?.toString() ?? "";
|
|
955
|
+
let parsed;
|
|
956
|
+
try {
|
|
957
|
+
parsed = await parseMime(raw);
|
|
958
|
+
} catch {
|
|
959
|
+
parsed = { body: {}, attachments: [] };
|
|
960
|
+
}
|
|
961
|
+
const enc = msg.envelope;
|
|
962
|
+
messages.push({
|
|
963
|
+
envelope: {
|
|
964
|
+
uid: msg.uid,
|
|
965
|
+
subject: enc?.subject ?? "",
|
|
966
|
+
from: addressArray2(enc?.from),
|
|
967
|
+
to: addressArray2(enc?.to),
|
|
968
|
+
...enc?.cc?.length ? { cc: addressArray2(enc.cc) } : {},
|
|
969
|
+
...enc?.bcc?.length ? { bcc: addressArray2(enc.bcc) } : {},
|
|
970
|
+
...enc?.replyTo?.length ? { replyTo: addressArray2(enc.replyTo) } : {},
|
|
971
|
+
date: (enc?.date ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
972
|
+
...enc?.messageId ? { messageId: enc.messageId } : {},
|
|
973
|
+
...enc?.inReplyTo ? { inReplyTo: enc.inReplyTo } : {},
|
|
974
|
+
flags: [...msg.flags ?? []],
|
|
975
|
+
size: msg.size ?? 0,
|
|
976
|
+
hasAttachments: parsed.attachments.length > 0
|
|
977
|
+
},
|
|
978
|
+
...parsed.body.text ? { text: parsed.body.text } : {},
|
|
979
|
+
...parsed.body.html ? { html: parsed.body.html } : {},
|
|
980
|
+
attachments: parsed.attachments
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
for (const uid of uidSet) notFound.push(uid);
|
|
984
|
+
return {
|
|
985
|
+
mailbox: input.mailbox,
|
|
986
|
+
messages,
|
|
987
|
+
notFound
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// src/tools/imap-read/get-quota.ts
|
|
993
|
+
import { z as z8 } from "zod";
|
|
994
|
+
var get_quota_default = defineTool({
|
|
995
|
+
name: "imap_get_quota",
|
|
996
|
+
description: "Quota-Info abfragen (RFC 2087)",
|
|
997
|
+
category: "imap-read",
|
|
998
|
+
inputSchema: z8.object({
|
|
999
|
+
account: z8.string().optional().describe("Account name (default: default_account)")
|
|
1000
|
+
}),
|
|
1001
|
+
handler: async (input, ctx) => {
|
|
1002
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1003
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1004
|
+
const caps = [...client.capabilities.keys()];
|
|
1005
|
+
if (!caps.some((c) => c.toUpperCase() === "QUOTA")) {
|
|
1006
|
+
return { root: "INBOX", usage: 0, limit: -1 };
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
const result = await client.getQuota();
|
|
1010
|
+
if (!result) {
|
|
1011
|
+
return { root: "INBOX", usage: 0, limit: -1 };
|
|
1012
|
+
}
|
|
1013
|
+
const usage = result.storage?.used ?? result.messages?.used ?? 0;
|
|
1014
|
+
const limit = result.storage?.limit ?? result.messages?.limit ?? -1;
|
|
1015
|
+
const resources = [];
|
|
1016
|
+
if (result.storage) {
|
|
1017
|
+
resources.push({
|
|
1018
|
+
name: "STORAGE",
|
|
1019
|
+
usage: result.storage.used,
|
|
1020
|
+
limit: result.storage.limit
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
if (result.messages) {
|
|
1024
|
+
resources.push({
|
|
1025
|
+
name: "MESSAGES",
|
|
1026
|
+
usage: result.messages.used,
|
|
1027
|
+
limit: result.messages.limit
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
root: result.path,
|
|
1032
|
+
usage,
|
|
1033
|
+
limit,
|
|
1034
|
+
...resources.length > 1 ? { resources } : {}
|
|
1035
|
+
};
|
|
1036
|
+
} catch {
|
|
1037
|
+
return { root: "INBOX", usage: 0, limit: -1 };
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// src/tools/imap-read/get-thread.ts
|
|
1043
|
+
import { z as z9 } from "zod";
|
|
1044
|
+
|
|
1045
|
+
// src/lib/threading.ts
|
|
1046
|
+
function reconstructThread(messages, rootUid) {
|
|
1047
|
+
if (messages.length === 0) return [];
|
|
1048
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
1049
|
+
const byMessageId = /* @__PURE__ */ new Map();
|
|
1050
|
+
for (const msg of messages) {
|
|
1051
|
+
byUid.set(msg.uid, msg);
|
|
1052
|
+
if (msg.messageId) {
|
|
1053
|
+
byMessageId.set(msg.messageId, msg);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const root = byUid.get(rootUid);
|
|
1057
|
+
if (!root) return [];
|
|
1058
|
+
const allMessageIds = /* @__PURE__ */ new Set();
|
|
1059
|
+
for (const msg of messages) {
|
|
1060
|
+
if (msg.messageId) allMessageIds.add(msg.messageId);
|
|
1061
|
+
if (msg.references) for (const ref of msg.references) allMessageIds.add(ref);
|
|
1062
|
+
if (msg.inReplyTo) allMessageIds.add(msg.inReplyTo);
|
|
1063
|
+
}
|
|
1064
|
+
const threadSet = /* @__PURE__ */ new Set();
|
|
1065
|
+
threadSet.add(rootUid);
|
|
1066
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1067
|
+
const queue = [rootUid];
|
|
1068
|
+
while (queue.length > 0) {
|
|
1069
|
+
const currentUid = queue.pop();
|
|
1070
|
+
if (seen.has(currentUid)) continue;
|
|
1071
|
+
seen.add(currentUid);
|
|
1072
|
+
const current = byUid.get(currentUid);
|
|
1073
|
+
if (!current) continue;
|
|
1074
|
+
const relevantIds = /* @__PURE__ */ new Set();
|
|
1075
|
+
if (current.messageId) relevantIds.add(current.messageId);
|
|
1076
|
+
if (current.inReplyTo) relevantIds.add(current.inReplyTo);
|
|
1077
|
+
if (current.references) for (const ref of current.references) relevantIds.add(ref);
|
|
1078
|
+
for (const other of messages) {
|
|
1079
|
+
if (threadSet.has(other.uid)) continue;
|
|
1080
|
+
if (!other.inReplyTo && !other.references) continue;
|
|
1081
|
+
if (other.inReplyTo && relevantIds.has(other.inReplyTo)) {
|
|
1082
|
+
threadSet.add(other.uid);
|
|
1083
|
+
queue.push(other.uid);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (other.references) {
|
|
1087
|
+
for (const ref of other.references) {
|
|
1088
|
+
if (relevantIds.has(ref)) {
|
|
1089
|
+
threadSet.add(other.uid);
|
|
1090
|
+
queue.push(other.uid);
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (current.messageId && other.references?.includes(current.messageId)) {
|
|
1096
|
+
threadSet.add(other.uid);
|
|
1097
|
+
queue.push(other.uid);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (current.inReplyTo) {
|
|
1101
|
+
const parent = byMessageId.get(current.inReplyTo);
|
|
1102
|
+
if (parent) {
|
|
1103
|
+
threadSet.add(parent.uid);
|
|
1104
|
+
queue.push(parent.uid);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (current.references) {
|
|
1108
|
+
for (const ref of current.references) {
|
|
1109
|
+
const refMsg = byMessageId.get(ref);
|
|
1110
|
+
if (refMsg) {
|
|
1111
|
+
threadSet.add(refMsg.uid);
|
|
1112
|
+
queue.push(refMsg.uid);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const result = messages.filter((m) => threadSet.has(m.uid));
|
|
1118
|
+
result.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/tools/imap-read/get-thread.ts
|
|
1123
|
+
function addressArray3(addr) {
|
|
1124
|
+
if (!addr) return [];
|
|
1125
|
+
const list = Array.isArray(addr) ? addr : [addr];
|
|
1126
|
+
const result = [];
|
|
1127
|
+
for (const entry of list) {
|
|
1128
|
+
if (entry && typeof entry === "object") {
|
|
1129
|
+
const e = entry;
|
|
1130
|
+
const addrStr = e.address ?? "";
|
|
1131
|
+
if (addrStr) {
|
|
1132
|
+
result.push({ ...e.name ? { name: e.name } : {}, address: addrStr });
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
var get_thread_default = defineTool({
|
|
1139
|
+
name: "imap_get_thread",
|
|
1140
|
+
description: "Konversation via In-Reply-To/References rekonstruieren",
|
|
1141
|
+
category: "imap-read",
|
|
1142
|
+
inputSchema: z9.object({
|
|
1143
|
+
mailbox: z9.string().min(1).describe("Mailbox path"),
|
|
1144
|
+
uid: z9.number().int().positive().describe("Root message UID"),
|
|
1145
|
+
account: z9.string().optional().describe("Account name (default: default_account)")
|
|
1146
|
+
}),
|
|
1147
|
+
handler: async (input, ctx) => {
|
|
1148
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1149
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1150
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
1151
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
1152
|
+
const allMessages = [];
|
|
1153
|
+
const extras = /* @__PURE__ */ new Map();
|
|
1154
|
+
for await (const msg of client.fetch("1:*", {
|
|
1155
|
+
uid: true,
|
|
1156
|
+
envelope: true,
|
|
1157
|
+
flags: true,
|
|
1158
|
+
size: true,
|
|
1159
|
+
headers: ["message-id", "in-reply-to", "references"]
|
|
1160
|
+
})) {
|
|
1161
|
+
const enc = msg.envelope;
|
|
1162
|
+
if (!enc) continue;
|
|
1163
|
+
const rawHeaders = msg.headers?.toString() ?? "";
|
|
1164
|
+
const refMatch = rawHeaders.match(/^references:\s*(.*)$/im);
|
|
1165
|
+
const refs = refMatch ? refMatch[1]?.trim().split(/\s+/).filter(Boolean) : void 0;
|
|
1166
|
+
allMessages.push({
|
|
1167
|
+
uid: msg.uid,
|
|
1168
|
+
messageId: enc.messageId ?? void 0,
|
|
1169
|
+
inReplyTo: enc.inReplyTo ?? void 0,
|
|
1170
|
+
references: refs,
|
|
1171
|
+
date: enc.date ?? /* @__PURE__ */ new Date()
|
|
1172
|
+
});
|
|
1173
|
+
extras.set(msg.uid, {
|
|
1174
|
+
subject: enc.subject ?? "",
|
|
1175
|
+
from: addressArray3(enc.from),
|
|
1176
|
+
to: addressArray3(enc.to),
|
|
1177
|
+
...enc.cc?.length ? { cc: addressArray3(enc.cc) } : {},
|
|
1178
|
+
...enc.bcc?.length ? { bcc: addressArray3(enc.bcc) } : {},
|
|
1179
|
+
...enc.replyTo?.length ? { replyTo: addressArray3(enc.replyTo) } : {},
|
|
1180
|
+
date: (enc.date ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
1181
|
+
...enc.messageId ? { messageId: enc.messageId } : {},
|
|
1182
|
+
...enc.inReplyTo ? { inReplyTo: enc.inReplyTo } : {},
|
|
1183
|
+
flags: [...msg.flags ?? []],
|
|
1184
|
+
size: msg.size ?? 0,
|
|
1185
|
+
hasAttachments: false
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const thread = reconstructThread(allMessages, input.uid);
|
|
1189
|
+
const root = allMessages.find((m) => m.uid === input.uid);
|
|
1190
|
+
if (!root) throw new UidNotFoundError(input.uid, input.mailbox);
|
|
1191
|
+
const messages = thread.map((m) => ({
|
|
1192
|
+
...extras.get(m.uid) ?? {}
|
|
1193
|
+
}));
|
|
1194
|
+
return {
|
|
1195
|
+
rootUid: input.uid,
|
|
1196
|
+
mailbox: input.mailbox,
|
|
1197
|
+
messages
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
// src/tools/imap-read/list-mailboxes.ts
|
|
1203
|
+
import { z as z10 } from "zod";
|
|
1204
|
+
var list_mailboxes_default = defineTool({
|
|
1205
|
+
name: "imap_list_mailboxes",
|
|
1206
|
+
description: "Folder enumerieren mit Special-Use-Flags (RFC 6154)",
|
|
1207
|
+
category: "imap-read",
|
|
1208
|
+
inputSchema: z10.object({
|
|
1209
|
+
account: z10.string().optional().describe("Account name (default: default_account)")
|
|
1210
|
+
}),
|
|
1211
|
+
handler: async (input, ctx) => {
|
|
1212
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1213
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1214
|
+
const mailboxes = await client.list();
|
|
1215
|
+
const result = mailboxes.map((mb) => {
|
|
1216
|
+
const flags = [...mb.flags || []];
|
|
1217
|
+
const specialUse = flags.find(
|
|
1218
|
+
(f) => [
|
|
1219
|
+
"\\Inbox",
|
|
1220
|
+
"\\Sent",
|
|
1221
|
+
"\\Drafts",
|
|
1222
|
+
"\\Trash",
|
|
1223
|
+
"\\Junk",
|
|
1224
|
+
"\\Archive",
|
|
1225
|
+
"\\All",
|
|
1226
|
+
"\\Flagged"
|
|
1227
|
+
].includes(f)
|
|
1228
|
+
);
|
|
1229
|
+
return {
|
|
1230
|
+
path: mb.path,
|
|
1231
|
+
delimiter: mb.delimiter,
|
|
1232
|
+
flags,
|
|
1233
|
+
...specialUse ? { specialUse } : {},
|
|
1234
|
+
subscribed: mb.subscribed
|
|
1235
|
+
};
|
|
1236
|
+
});
|
|
1237
|
+
return { mailboxes: result };
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// src/tools/imap-read/list-messages.ts
|
|
1242
|
+
import { z as z11 } from "zod";
|
|
1243
|
+
function addressArray4(addr) {
|
|
1244
|
+
if (!addr) return [];
|
|
1245
|
+
const list = Array.isArray(addr) ? addr : [addr];
|
|
1246
|
+
const result = [];
|
|
1247
|
+
for (const entry of list) {
|
|
1248
|
+
if (entry && typeof entry === "object") {
|
|
1249
|
+
const e = entry;
|
|
1250
|
+
const addrStr = e.address ?? "";
|
|
1251
|
+
if (addrStr) {
|
|
1252
|
+
result.push({ ...e.name ? { name: e.name } : {}, address: addrStr });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
var list_messages_default = defineTool({
|
|
1259
|
+
name: "imap_list_messages",
|
|
1260
|
+
description: "Paginierte Envelope-Liste in einem Folder",
|
|
1261
|
+
category: "imap-read",
|
|
1262
|
+
inputSchema: z11.object({
|
|
1263
|
+
mailbox: z11.string().min(1).describe("Mailbox path"),
|
|
1264
|
+
page: z11.number().int().positive().default(1).describe("Page number (default: 1)"),
|
|
1265
|
+
pageSize: z11.number().int().positive().max(500).default(50).describe("Messages per page (default: 50, max: 500)"),
|
|
1266
|
+
sort: z11.enum(["date", "subject", "from", "size"]).optional().describe("Sort field"),
|
|
1267
|
+
sortOrder: z11.enum(["asc", "desc"]).default("desc").describe("Sort order (default: desc)"),
|
|
1268
|
+
account: z11.string().optional().describe("Account name (default: default_account)")
|
|
1269
|
+
}),
|
|
1270
|
+
handler: async (input, ctx) => {
|
|
1271
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1272
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1273
|
+
const mailbox = await client.mailboxOpen(input.mailbox);
|
|
1274
|
+
if (!mailbox) {
|
|
1275
|
+
throw new MailboxNotFoundError(input.mailbox);
|
|
1276
|
+
}
|
|
1277
|
+
const total = mailbox.exists;
|
|
1278
|
+
const currentPage = input.page ?? 1;
|
|
1279
|
+
const pageSize = input.pageSize ?? 50;
|
|
1280
|
+
const startNum = (currentPage - 1) * pageSize + 1;
|
|
1281
|
+
const endNum = Math.min(startNum + pageSize - 1, total);
|
|
1282
|
+
if (startNum > total) {
|
|
1283
|
+
return {
|
|
1284
|
+
mailbox: input.mailbox,
|
|
1285
|
+
page: currentPage,
|
|
1286
|
+
pageSize,
|
|
1287
|
+
total,
|
|
1288
|
+
messages: []
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const messages = [];
|
|
1292
|
+
for await (const msg of client.fetch(`${startNum}:${endNum}`, {
|
|
1293
|
+
uid: true,
|
|
1294
|
+
envelope: true,
|
|
1295
|
+
flags: true,
|
|
1296
|
+
size: true
|
|
1297
|
+
})) {
|
|
1298
|
+
const enc = msg.envelope;
|
|
1299
|
+
if (!enc) continue;
|
|
1300
|
+
let hasAttachments = false;
|
|
1301
|
+
if (msg.bodyStructure?.type?.toLowerCase() === "multipart") {
|
|
1302
|
+
hasAttachments = true;
|
|
1303
|
+
}
|
|
1304
|
+
messages.push({
|
|
1305
|
+
uid: msg.uid,
|
|
1306
|
+
seq: msg.seq,
|
|
1307
|
+
subject: enc.subject ?? "",
|
|
1308
|
+
from: addressArray4(enc.from),
|
|
1309
|
+
to: addressArray4(enc.to),
|
|
1310
|
+
...enc.cc?.length ? { cc: addressArray4(enc.cc) } : {},
|
|
1311
|
+
...enc.bcc?.length ? { bcc: addressArray4(enc.bcc) } : {},
|
|
1312
|
+
...enc.replyTo?.length ? { replyTo: addressArray4(enc.replyTo) } : {},
|
|
1313
|
+
date: (enc.date ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
1314
|
+
...enc.messageId ? { messageId: enc.messageId } : {},
|
|
1315
|
+
...enc.inReplyTo ? { inReplyTo: enc.inReplyTo } : {},
|
|
1316
|
+
flags: [...msg.flags ?? []],
|
|
1317
|
+
size: msg.size ?? 0,
|
|
1318
|
+
hasAttachments
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
mailbox: input.mailbox,
|
|
1323
|
+
page: currentPage,
|
|
1324
|
+
pageSize,
|
|
1325
|
+
total,
|
|
1326
|
+
messages
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// src/tools/imap-read/search.ts
|
|
1332
|
+
import { z as z12 } from "zod";
|
|
1333
|
+
|
|
1334
|
+
// src/lib/search-builder.ts
|
|
1335
|
+
function parseSearchDate(value) {
|
|
1336
|
+
const isoMatch = value.match(/^\d{4}-\d{2}-\d{2}/);
|
|
1337
|
+
if (isoMatch) return new Date(isoMatch[0]);
|
|
1338
|
+
const parsed = new Date(value);
|
|
1339
|
+
if (!Number.isNaN(parsed.getTime())) return parsed;
|
|
1340
|
+
const months = {
|
|
1341
|
+
jan: "01",
|
|
1342
|
+
feb: "02",
|
|
1343
|
+
mar: "03",
|
|
1344
|
+
apr: "04",
|
|
1345
|
+
may: "05",
|
|
1346
|
+
jun: "06",
|
|
1347
|
+
jul: "07",
|
|
1348
|
+
aug: "08",
|
|
1349
|
+
sep: "09",
|
|
1350
|
+
oct: "10",
|
|
1351
|
+
nov: "11",
|
|
1352
|
+
dec: "12"
|
|
1353
|
+
};
|
|
1354
|
+
const parts = value.split("-");
|
|
1355
|
+
if (parts.length === 3) {
|
|
1356
|
+
const day = parts[0].padStart(2, "0");
|
|
1357
|
+
const monKey = parts[1]?.toLowerCase().slice(0, 3);
|
|
1358
|
+
const mon = monKey ? months[monKey] : void 0;
|
|
1359
|
+
const yearRaw = parts[2];
|
|
1360
|
+
const year = yearRaw.length === 2 ? `20${yearRaw}` : yearRaw;
|
|
1361
|
+
if (mon && day) return /* @__PURE__ */ new Date(`${year}-${mon}-${day}`);
|
|
1362
|
+
}
|
|
1363
|
+
throw new Error(
|
|
1364
|
+
`Cannot parse date: ${value}. Use DD-Mon-YYYY (e.g., 21-May-2026) or YYYY-MM-DD.`
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
function buildSearchQuery(criteria) {
|
|
1368
|
+
const query = {};
|
|
1369
|
+
if (criteria.from !== void 0) query.from = criteria.from;
|
|
1370
|
+
if (criteria.to !== void 0) query.to = criteria.to;
|
|
1371
|
+
if (criteria.cc !== void 0) query.cc = criteria.cc;
|
|
1372
|
+
if (criteria.bcc !== void 0) query.bcc = criteria.bcc;
|
|
1373
|
+
if (criteria.subject !== void 0) query.subject = criteria.subject;
|
|
1374
|
+
if (criteria.body !== void 0) query.body = criteria.body;
|
|
1375
|
+
if (criteria.text !== void 0) query.text = criteria.text;
|
|
1376
|
+
if (criteria.since !== void 0) query.since = parseSearchDate(criteria.since);
|
|
1377
|
+
if (criteria.before !== void 0) query.before = parseSearchDate(criteria.before);
|
|
1378
|
+
if (criteria.on !== void 0) query.on = parseSearchDate(criteria.on);
|
|
1379
|
+
if (criteria.sentSince !== void 0) query.sentSince = parseSearchDate(criteria.sentSince);
|
|
1380
|
+
if (criteria.sentBefore !== void 0) query.sentBefore = parseSearchDate(criteria.sentBefore);
|
|
1381
|
+
if (criteria.sentOn !== void 0) query.sentOn = parseSearchDate(criteria.sentOn);
|
|
1382
|
+
if (criteria.larger !== void 0) query.larger = criteria.larger;
|
|
1383
|
+
if (criteria.smaller !== void 0) query.smaller = criteria.smaller;
|
|
1384
|
+
if (criteria.unseen !== void 0) query.unseen = criteria.unseen;
|
|
1385
|
+
if (criteria.seen !== void 0) query.seen = criteria.seen;
|
|
1386
|
+
if (criteria.flagged !== void 0) query.flagged = criteria.flagged;
|
|
1387
|
+
if (criteria.unflagged !== void 0) query.unflagged = criteria.unflagged;
|
|
1388
|
+
if (criteria.answered !== void 0) query.answered = criteria.answered;
|
|
1389
|
+
if (criteria.unanswered !== void 0) query.unanswered = criteria.unanswered;
|
|
1390
|
+
if (criteria.deleted !== void 0) query.deleted = criteria.deleted;
|
|
1391
|
+
if (criteria.undeleted !== void 0) query.undeleted = criteria.undeleted;
|
|
1392
|
+
if (criteria.new !== void 0) query.new = criteria.new;
|
|
1393
|
+
if (criteria.old !== void 0) query.old = criteria.old;
|
|
1394
|
+
if (criteria.recent !== void 0) query.recent = criteria.recent;
|
|
1395
|
+
if (criteria.keyword !== void 0) query.keyword = criteria.keyword;
|
|
1396
|
+
if (criteria.unkeyword !== void 0) query.unkeyword = criteria.unkeyword;
|
|
1397
|
+
return query;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/tools/imap-read/search.ts
|
|
1401
|
+
var search_default = defineTool({
|
|
1402
|
+
name: "imap_search",
|
|
1403
|
+
description: "Vollst\xE4ndiger RFC-3501-SEARCH-Builder",
|
|
1404
|
+
category: "imap-read",
|
|
1405
|
+
inputSchema: z12.object({
|
|
1406
|
+
mailbox: z12.string().min(1).describe("Mailbox path"),
|
|
1407
|
+
from: z12.string().optional().describe("Sender address"),
|
|
1408
|
+
to: z12.string().optional().describe("Recipient address"),
|
|
1409
|
+
cc: z12.string().optional().describe("CC recipient address"),
|
|
1410
|
+
bcc: z12.string().optional().describe("BCC recipient address"),
|
|
1411
|
+
subject: z12.string().optional().describe("Subject line"),
|
|
1412
|
+
body: z12.string().optional().describe("Body text"),
|
|
1413
|
+
text: z12.string().optional().describe("Full text (headers + body)"),
|
|
1414
|
+
since: z12.string().optional().describe("Messages on or after this date (DD-Mon-YYYY or YYYY-MM-DD)"),
|
|
1415
|
+
before: z12.string().optional().describe("Messages before this date"),
|
|
1416
|
+
on: z12.string().optional().describe("Messages on this exact date"),
|
|
1417
|
+
sentSince: z12.string().optional().describe("Sent on or after this date"),
|
|
1418
|
+
sentBefore: z12.string().optional().describe("Sent before this date"),
|
|
1419
|
+
sentOn: z12.string().optional().describe("Sent on this exact date"),
|
|
1420
|
+
larger: z12.number().int().positive().optional().describe("Larger than N bytes"),
|
|
1421
|
+
smaller: z12.number().int().positive().optional().describe("Smaller than N bytes"),
|
|
1422
|
+
unseen: z12.boolean().optional().describe("Not read"),
|
|
1423
|
+
seen: z12.boolean().optional().describe("Read"),
|
|
1424
|
+
flagged: z12.boolean().optional().describe("Flagged"),
|
|
1425
|
+
unflagged: z12.boolean().optional().describe("Not flagged"),
|
|
1426
|
+
answered: z12.boolean().optional().describe("Answered"),
|
|
1427
|
+
unanswered: z12.boolean().optional().describe("Not answered"),
|
|
1428
|
+
deleted: z12.boolean().optional().describe("Deleted"),
|
|
1429
|
+
undeleted: z12.boolean().optional().describe("Not deleted"),
|
|
1430
|
+
keyword: z12.string().optional().describe("Has this keyword"),
|
|
1431
|
+
unkeyword: z12.string().optional().describe("Does not have this keyword"),
|
|
1432
|
+
new: z12.boolean().optional().describe("New (unseen + recent)"),
|
|
1433
|
+
old: z12.boolean().optional().describe("Old (not recent)"),
|
|
1434
|
+
recent: z12.boolean().optional().describe("Recent"),
|
|
1435
|
+
account: z12.string().optional().describe("Account name (default: default_account)")
|
|
1436
|
+
}),
|
|
1437
|
+
handler: async (input, ctx) => {
|
|
1438
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1439
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1440
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
1441
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
1442
|
+
const { mailbox: _mb, account: _acct, ...searchCriteria } = input;
|
|
1443
|
+
const query = buildSearchQuery(
|
|
1444
|
+
searchCriteria
|
|
1445
|
+
);
|
|
1446
|
+
const result = await client.search(query);
|
|
1447
|
+
const uids = result ? [...result] : [];
|
|
1448
|
+
return {
|
|
1449
|
+
mailbox: input.mailbox,
|
|
1450
|
+
uids,
|
|
1451
|
+
count: uids.length
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
// src/tools/imap-read/status-mailbox.ts
|
|
1457
|
+
import { z as z13 } from "zod";
|
|
1458
|
+
var status_mailbox_default = defineTool({
|
|
1459
|
+
name: "imap_status_mailbox",
|
|
1460
|
+
description: "Counts (unread/total/recent) ohne SELECT (STATUS)",
|
|
1461
|
+
category: "imap-read",
|
|
1462
|
+
inputSchema: z13.object({
|
|
1463
|
+
mailbox: z13.string().min(1).describe("Mailbox path (e.g. INBOX)"),
|
|
1464
|
+
account: z13.string().optional().describe("Account name (default: default_account)")
|
|
1465
|
+
}),
|
|
1466
|
+
handler: async (input, ctx) => {
|
|
1467
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1468
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1469
|
+
try {
|
|
1470
|
+
const status = await client.status(input.mailbox, {
|
|
1471
|
+
messages: true,
|
|
1472
|
+
unseen: true,
|
|
1473
|
+
recent: true,
|
|
1474
|
+
uidNext: true,
|
|
1475
|
+
uidValidity: true
|
|
1476
|
+
});
|
|
1477
|
+
return {
|
|
1478
|
+
path: input.mailbox,
|
|
1479
|
+
messages: status.messages ?? 0,
|
|
1480
|
+
unseen: status.unseen ?? 0,
|
|
1481
|
+
recent: status.recent ?? 0,
|
|
1482
|
+
...status.uidNext !== void 0 ? { uidNext: status.uidNext } : {},
|
|
1483
|
+
...status.uidValidity !== void 0 ? { uidValidity: status.uidValidity } : {}
|
|
1484
|
+
};
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
throw new MailboxNotFoundError(input.mailbox);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
// src/tools/imap-write/append-message.ts
|
|
1492
|
+
import { z as z14 } from "zod";
|
|
1493
|
+
var append_message_default = defineTool({
|
|
1494
|
+
name: "imap_append_message",
|
|
1495
|
+
description: "Mail in Folder schreiben (APPEND, Drafts/Import)",
|
|
1496
|
+
category: "imap-write",
|
|
1497
|
+
inputSchema: z14.object({
|
|
1498
|
+
mailbox: z14.string().min(1).describe("Target mailbox path"),
|
|
1499
|
+
raw: z14.string().min(1).describe("RFC-822 raw message content"),
|
|
1500
|
+
flags: z14.array(z14.string()).optional().describe("Optional flags to set (e.g. ['\\Seen', '\\Drafts'])"),
|
|
1501
|
+
date: z14.string().optional().describe("Optional internal date (ISO 8601 or imap date-time)"),
|
|
1502
|
+
account: z14.string().optional().describe("Account name (default: default_account)")
|
|
1503
|
+
}),
|
|
1504
|
+
handler: async (input, ctx) => {
|
|
1505
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1506
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1507
|
+
const normalizedFlags = input.flags?.map((f) => f.startsWith("\\") ? f : `\\${f}`);
|
|
1508
|
+
const internalDate = input.date ? new Date(input.date) : void 0;
|
|
1509
|
+
try {
|
|
1510
|
+
const result = await client.append(input.mailbox, input.raw, normalizedFlags, internalDate);
|
|
1511
|
+
const uid = result && result.uid !== void 0 ? Number(result.uid) : void 0;
|
|
1512
|
+
return {
|
|
1513
|
+
mailbox: input.mailbox,
|
|
1514
|
+
...uid !== void 0 ? { uid } : {}
|
|
1515
|
+
};
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
throw new ImapProtocolError(`Failed to append message: ${err}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
// src/tools/imap-write/bulk-mark.ts
|
|
1523
|
+
import { z as z15 } from "zod";
|
|
1524
|
+
var bulk_mark_default = defineTool({
|
|
1525
|
+
name: "imap_bulk_mark",
|
|
1526
|
+
description: "Bulk-STORE \xFCber mehrere UIDs",
|
|
1527
|
+
category: "imap-write",
|
|
1528
|
+
inputSchema: z15.object({
|
|
1529
|
+
mailbox: z15.string().min(1).describe("Mailbox path"),
|
|
1530
|
+
uids: z15.array(z15.number().int().positive()).min(1).max(500).describe("Message UIDs"),
|
|
1531
|
+
flags: z15.array(z15.string()).min(1).describe("Flags to set/add/remove"),
|
|
1532
|
+
mode: z15.enum(["set", "add", "remove"]).default("set").describe("STORE mode"),
|
|
1533
|
+
account: z15.string().optional().describe("Account name (default: default_account)")
|
|
1534
|
+
}),
|
|
1535
|
+
handler: async (input, ctx) => {
|
|
1536
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1537
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1538
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
1539
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
1540
|
+
const normalizedFlags = input.flags.map(
|
|
1541
|
+
(f) => f.startsWith("\\") ? f : `\\${f}`
|
|
1542
|
+
);
|
|
1543
|
+
const notFound = [];
|
|
1544
|
+
let modified = 0;
|
|
1545
|
+
for (const uid of input.uids) {
|
|
1546
|
+
try {
|
|
1547
|
+
switch (input.mode) {
|
|
1548
|
+
case "set":
|
|
1549
|
+
await client.messageFlagsSet(uid, normalizedFlags);
|
|
1550
|
+
break;
|
|
1551
|
+
case "add":
|
|
1552
|
+
await client.messageFlagsAdd(uid, normalizedFlags);
|
|
1553
|
+
break;
|
|
1554
|
+
case "remove":
|
|
1555
|
+
await client.messageFlagsRemove(uid, normalizedFlags);
|
|
1556
|
+
break;
|
|
1557
|
+
}
|
|
1558
|
+
modified++;
|
|
1559
|
+
} catch {
|
|
1560
|
+
notFound.push(uid);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
modified,
|
|
1565
|
+
uids: input.uids.filter((u) => !notFound.includes(u)),
|
|
1566
|
+
notFound
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// src/tools/imap-write/bulk-move.ts
|
|
1572
|
+
import { z as z16 } from "zod";
|
|
1573
|
+
var bulk_move_default = defineTool({
|
|
1574
|
+
name: "imap_bulk_move",
|
|
1575
|
+
description: "Bulk-MOVE \xFCber mehrere UIDs",
|
|
1576
|
+
category: "imap-write",
|
|
1577
|
+
inputSchema: z16.object({
|
|
1578
|
+
fromMailbox: z16.string().min(1).describe("Source mailbox path"),
|
|
1579
|
+
toMailbox: z16.string().min(1).describe("Target mailbox path"),
|
|
1580
|
+
uids: z16.array(z16.number().int().positive()).min(1).max(500).describe("Message UIDs to move"),
|
|
1581
|
+
account: z16.string().optional().describe("Account name (default: default_account)")
|
|
1582
|
+
}),
|
|
1583
|
+
handler: async (input, ctx) => {
|
|
1584
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1585
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1586
|
+
const mb = await client.mailboxOpen(input.fromMailbox);
|
|
1587
|
+
if (!mb) throw new MailboxNotFoundError(input.fromMailbox);
|
|
1588
|
+
const moved = [];
|
|
1589
|
+
const notFound = [];
|
|
1590
|
+
try {
|
|
1591
|
+
await client.messageMove(input.uids, input.toMailbox);
|
|
1592
|
+
moved.push(...input.uids);
|
|
1593
|
+
} catch {
|
|
1594
|
+
for (const uid of input.uids) {
|
|
1595
|
+
try {
|
|
1596
|
+
await client.messageMove(uid, input.toMailbox);
|
|
1597
|
+
moved.push(uid);
|
|
1598
|
+
} catch {
|
|
1599
|
+
try {
|
|
1600
|
+
await client.messageCopy(uid, input.toMailbox);
|
|
1601
|
+
await client.messageDelete(uid);
|
|
1602
|
+
moved.push(uid);
|
|
1603
|
+
} catch {
|
|
1604
|
+
notFound.push(uid);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
fromMailbox: input.fromMailbox,
|
|
1611
|
+
toMailbox: input.toMailbox,
|
|
1612
|
+
moved: moved.length,
|
|
1613
|
+
uids: moved,
|
|
1614
|
+
notFound
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// src/tools/imap-write/copy-message.ts
|
|
1620
|
+
import { z as z17 } from "zod";
|
|
1621
|
+
var copy_message_default = defineTool({
|
|
1622
|
+
name: "imap_copy_message",
|
|
1623
|
+
description: "Mail kopieren (COPY)",
|
|
1624
|
+
category: "imap-write",
|
|
1625
|
+
inputSchema: z17.object({
|
|
1626
|
+
fromMailbox: z17.string().min(1).describe("Source mailbox path"),
|
|
1627
|
+
toMailbox: z17.string().min(1).describe("Target mailbox path"),
|
|
1628
|
+
uid: z17.number().int().positive().describe("Message UID"),
|
|
1629
|
+
account: z17.string().optional().describe("Account name (default: default_account)")
|
|
1630
|
+
}),
|
|
1631
|
+
handler: async (input, ctx) => {
|
|
1632
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1633
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1634
|
+
const mb = await client.mailboxOpen(input.fromMailbox);
|
|
1635
|
+
if (!mb) throw new MailboxNotFoundError(input.fromMailbox);
|
|
1636
|
+
let targetUid;
|
|
1637
|
+
try {
|
|
1638
|
+
const result = await client.messageCopy(input.uid, input.toMailbox);
|
|
1639
|
+
if (result && result.uidMap) {
|
|
1640
|
+
targetUid = result.uidMap.get(input.uid);
|
|
1641
|
+
}
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
throw new ImapProtocolError(`Failed to copy message: ${err}`);
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
fromMailbox: input.fromMailbox,
|
|
1647
|
+
toMailbox: input.toMailbox,
|
|
1648
|
+
sourceUid: input.uid,
|
|
1649
|
+
...targetUid !== void 0 ? { targetUid } : {}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
// src/tools/imap-write/delete-message.ts
|
|
1655
|
+
import { z as z18 } from "zod";
|
|
1656
|
+
var delete_message_default = defineTool({
|
|
1657
|
+
name: "imap_delete_message",
|
|
1658
|
+
description: "STORE \\Deleted + optional EXPUNGE",
|
|
1659
|
+
category: "imap-write",
|
|
1660
|
+
inputSchema: z18.object({
|
|
1661
|
+
mailbox: z18.string().min(1).describe("Mailbox path"),
|
|
1662
|
+
uid: z18.number().int().positive().describe("Message UID"),
|
|
1663
|
+
expunge: z18.boolean().default(false).describe("Also EXPUNGE after marking (default: false)"),
|
|
1664
|
+
account: z18.string().optional().describe("Account name (default: default_account)")
|
|
1665
|
+
}),
|
|
1666
|
+
handler: async (input, ctx) => {
|
|
1667
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1668
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1669
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
1670
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
1671
|
+
try {
|
|
1672
|
+
await client.messageDelete(input.uid);
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
throw new UidNotFoundError(input.uid, input.mailbox);
|
|
1675
|
+
}
|
|
1676
|
+
let expunged = false;
|
|
1677
|
+
if (input.expunge) {
|
|
1678
|
+
try {
|
|
1679
|
+
await client.mailboxClose();
|
|
1680
|
+
expunged = true;
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
throw new ImapProtocolError(`Failed to expunge after delete: ${err}`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
uid: input.uid,
|
|
1687
|
+
mailbox: input.mailbox,
|
|
1688
|
+
expunged
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
// src/tools/imap-write/expunge.ts
|
|
1694
|
+
import { z as z19 } from "zod";
|
|
1695
|
+
var expunge_default = defineTool({
|
|
1696
|
+
name: "imap_expunge",
|
|
1697
|
+
description: "EXPUNGE eines Folders",
|
|
1698
|
+
category: "imap-write",
|
|
1699
|
+
inputSchema: z19.object({
|
|
1700
|
+
mailbox: z19.string().min(1).describe("Mailbox path to expunge"),
|
|
1701
|
+
account: z19.string().optional().describe("Account name (default: default_account)")
|
|
1702
|
+
}),
|
|
1703
|
+
handler: async (input, ctx) => {
|
|
1704
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1705
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1706
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
1707
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
1708
|
+
const beforeCount = mb.exists;
|
|
1709
|
+
try {
|
|
1710
|
+
await client.mailboxClose();
|
|
1711
|
+
const after = await client.mailboxOpen(input.mailbox);
|
|
1712
|
+
const afterCount = after?.exists ?? beforeCount;
|
|
1713
|
+
return {
|
|
1714
|
+
mailbox: input.mailbox,
|
|
1715
|
+
expunged: Math.max(0, beforeCount - afterCount)
|
|
1716
|
+
};
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
throw new ImapProtocolError(`Failed to expunge mailbox: ${err}`);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
// src/tools/imap-write/mark-message.ts
|
|
1724
|
+
import { z as z20 } from "zod";
|
|
1725
|
+
var mark_message_default = defineTool({
|
|
1726
|
+
name: "imap_mark_message",
|
|
1727
|
+
description: "Flags setzen/entfernen (STORE)",
|
|
1728
|
+
category: "imap-write",
|
|
1729
|
+
inputSchema: z20.object({
|
|
1730
|
+
mailbox: z20.string().min(1).describe("Mailbox path"),
|
|
1731
|
+
uid: z20.number().int().positive().describe("Message UID"),
|
|
1732
|
+
flags: z20.array(z20.string()).min(1).describe("Flags to set/add/remove (e.g. ['\\Seen', '\\Flagged'])"),
|
|
1733
|
+
mode: z20.enum(["set", "add", "remove"]).default("set").describe("STORE mode: set (replace), add (+FLAGS), remove (-FLAGS)"),
|
|
1734
|
+
account: z20.string().optional().describe("Account name (default: default_account)")
|
|
1735
|
+
}),
|
|
1736
|
+
handler: async (input, ctx) => {
|
|
1737
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1738
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1739
|
+
const mb = await client.mailboxOpen(input.mailbox);
|
|
1740
|
+
if (!mb) throw new MailboxNotFoundError(input.mailbox);
|
|
1741
|
+
const normalizedFlags = input.flags.map(
|
|
1742
|
+
(f) => f.startsWith("\\") ? f : `\\${f}`
|
|
1743
|
+
);
|
|
1744
|
+
try {
|
|
1745
|
+
switch (input.mode) {
|
|
1746
|
+
case "set":
|
|
1747
|
+
await client.messageFlagsSet(input.uid, normalizedFlags);
|
|
1748
|
+
break;
|
|
1749
|
+
case "add":
|
|
1750
|
+
await client.messageFlagsAdd(input.uid, normalizedFlags);
|
|
1751
|
+
break;
|
|
1752
|
+
case "remove":
|
|
1753
|
+
await client.messageFlagsRemove(input.uid, normalizedFlags);
|
|
1754
|
+
break;
|
|
1755
|
+
}
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
throw new ImapProtocolError(`Failed to set flags: ${err}`);
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
uid: input.uid,
|
|
1761
|
+
flags: input.flags
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
// src/tools/imap-write/move-message.ts
|
|
1767
|
+
import { z as z21 } from "zod";
|
|
1768
|
+
var move_message_default = defineTool({
|
|
1769
|
+
name: "imap_move_message",
|
|
1770
|
+
description: "Mail verschieben (MOVE, Fallback COPY+EXPUNGE)",
|
|
1771
|
+
category: "imap-write",
|
|
1772
|
+
inputSchema: z21.object({
|
|
1773
|
+
fromMailbox: z21.string().min(1).describe("Source mailbox path"),
|
|
1774
|
+
toMailbox: z21.string().min(1).describe("Target mailbox path"),
|
|
1775
|
+
uid: z21.number().int().positive().describe("Message UID"),
|
|
1776
|
+
account: z21.string().optional().describe("Account name (default: default_account)")
|
|
1777
|
+
}),
|
|
1778
|
+
handler: async (input, ctx) => {
|
|
1779
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1780
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1781
|
+
const mb = await client.mailboxOpen(input.fromMailbox);
|
|
1782
|
+
if (!mb) throw new MailboxNotFoundError(input.fromMailbox);
|
|
1783
|
+
let targetUid;
|
|
1784
|
+
try {
|
|
1785
|
+
const result = await client.messageMove(input.uid, input.toMailbox);
|
|
1786
|
+
if (result && result.uidMap) {
|
|
1787
|
+
targetUid = result.uidMap.get(input.uid);
|
|
1788
|
+
}
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
try {
|
|
1791
|
+
const copyResult = await client.messageCopy(input.uid, input.toMailbox);
|
|
1792
|
+
if (copyResult && copyResult.uidMap) {
|
|
1793
|
+
targetUid = copyResult.uidMap.get(input.uid);
|
|
1794
|
+
}
|
|
1795
|
+
await client.messageDelete(input.uid);
|
|
1796
|
+
} catch (fallbackErr) {
|
|
1797
|
+
throw new ImapProtocolError(`Failed to move message: ${fallbackErr}`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return {
|
|
1801
|
+
fromMailbox: input.fromMailbox,
|
|
1802
|
+
toMailbox: input.toMailbox,
|
|
1803
|
+
sourceUid: input.uid,
|
|
1804
|
+
...targetUid !== void 0 ? { targetUid } : {}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
// src/tools/imap-mailbox/create-mailbox.ts
|
|
1810
|
+
import { z as z22 } from "zod";
|
|
1811
|
+
var create_mailbox_default = defineTool({
|
|
1812
|
+
name: "imap_create_mailbox",
|
|
1813
|
+
description: "Neuen Folder anlegen (CREATE)",
|
|
1814
|
+
category: "imap-mailbox",
|
|
1815
|
+
inputSchema: z22.object({
|
|
1816
|
+
path: z22.string().min(1).describe("Mailbox path to create"),
|
|
1817
|
+
account: z22.string().optional().describe("Account name (default: default_account)")
|
|
1818
|
+
}),
|
|
1819
|
+
handler: async (input, ctx) => {
|
|
1820
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1821
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1822
|
+
try {
|
|
1823
|
+
await client.mailboxCreate(input.path);
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
throw new ImapProtocolError(`Failed to create mailbox: ${err}`);
|
|
1826
|
+
}
|
|
1827
|
+
return { path: input.path, created: true };
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
// src/tools/imap-mailbox/delete-mailbox.ts
|
|
1832
|
+
import { z as z23 } from "zod";
|
|
1833
|
+
var delete_mailbox_default = defineTool({
|
|
1834
|
+
name: "imap_delete_mailbox",
|
|
1835
|
+
description: "Folder l\xF6schen (DELETE)",
|
|
1836
|
+
category: "imap-mailbox",
|
|
1837
|
+
inputSchema: z23.object({
|
|
1838
|
+
path: z23.string().min(1).describe("Mailbox path to delete"),
|
|
1839
|
+
account: z23.string().optional().describe("Account name (default: default_account)")
|
|
1840
|
+
}),
|
|
1841
|
+
handler: async (input, ctx) => {
|
|
1842
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1843
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1844
|
+
try {
|
|
1845
|
+
await client.mailboxDelete(input.path);
|
|
1846
|
+
} catch (err) {
|
|
1847
|
+
throw new ImapProtocolError(`Failed to delete mailbox: ${err}`);
|
|
1848
|
+
}
|
|
1849
|
+
return { path: input.path, deleted: true };
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
// src/tools/imap-mailbox/rename-mailbox.ts
|
|
1854
|
+
import { z as z24 } from "zod";
|
|
1855
|
+
var rename_mailbox_default = defineTool({
|
|
1856
|
+
name: "imap_rename_mailbox",
|
|
1857
|
+
description: "Folder umbenennen (RENAME)",
|
|
1858
|
+
category: "imap-mailbox",
|
|
1859
|
+
inputSchema: z24.object({
|
|
1860
|
+
from: z24.string().min(1).describe("Current mailbox path"),
|
|
1861
|
+
to: z24.string().min(1).describe("New mailbox path"),
|
|
1862
|
+
account: z24.string().optional().describe("Account name (default: default_account)")
|
|
1863
|
+
}),
|
|
1864
|
+
handler: async (input, ctx) => {
|
|
1865
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1866
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1867
|
+
try {
|
|
1868
|
+
await client.mailboxRename(input.from, input.to);
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
throw new ImapProtocolError(`Failed to rename mailbox: ${err}`);
|
|
1871
|
+
}
|
|
1872
|
+
return { from: input.from, to: input.to, renamed: true };
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
// src/tools/imap-mailbox/subscribe-mailbox.ts
|
|
1877
|
+
import { z as z25 } from "zod";
|
|
1878
|
+
var subscribe_mailbox_default = defineTool({
|
|
1879
|
+
name: "imap_subscribe_mailbox",
|
|
1880
|
+
description: "Folder abonnieren (SUBSCRIBE)",
|
|
1881
|
+
category: "imap-mailbox",
|
|
1882
|
+
inputSchema: z25.object({
|
|
1883
|
+
path: z25.string().min(1).describe("Mailbox path to subscribe"),
|
|
1884
|
+
account: z25.string().optional().describe("Account name (default: default_account)")
|
|
1885
|
+
}),
|
|
1886
|
+
handler: async (input, ctx) => {
|
|
1887
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1888
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1889
|
+
try {
|
|
1890
|
+
await client.mailboxSubscribe(input.path);
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
throw new ImapProtocolError(`Failed to subscribe to mailbox: ${err}`);
|
|
1893
|
+
}
|
|
1894
|
+
return { path: input.path, subscribed: true };
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
// src/tools/imap-mailbox/unsubscribe-mailbox.ts
|
|
1899
|
+
import { z as z26 } from "zod";
|
|
1900
|
+
var unsubscribe_mailbox_default = defineTool({
|
|
1901
|
+
name: "imap_unsubscribe_mailbox",
|
|
1902
|
+
description: "Folder-Abo entfernen (UNSUBSCRIBE)",
|
|
1903
|
+
category: "imap-mailbox",
|
|
1904
|
+
inputSchema: z26.object({
|
|
1905
|
+
path: z26.string().min(1).describe("Mailbox path to unsubscribe"),
|
|
1906
|
+
account: z26.string().optional().describe("Account name (default: default_account)")
|
|
1907
|
+
}),
|
|
1908
|
+
handler: async (input, ctx) => {
|
|
1909
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1910
|
+
const client = await ctx.imap.acquire(accountName);
|
|
1911
|
+
try {
|
|
1912
|
+
await client.mailboxUnsubscribe(input.path);
|
|
1913
|
+
} catch (err) {
|
|
1914
|
+
throw new ImapProtocolError(`Failed to unsubscribe from mailbox: ${err}`);
|
|
1915
|
+
}
|
|
1916
|
+
return { path: input.path, subscribed: false };
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
// src/tools/smtp/forward.ts
|
|
1921
|
+
import { z as z27 } from "zod";
|
|
1922
|
+
|
|
1923
|
+
// src/lib/sent-folder.ts
|
|
1924
|
+
var SENT_FALLBACKS = [
|
|
1925
|
+
"Sent",
|
|
1926
|
+
"Sent Items",
|
|
1927
|
+
"[Gmail]/Sent Mail",
|
|
1928
|
+
"[Gmail]/Gesendet",
|
|
1929
|
+
"INBOX.Sent",
|
|
1930
|
+
"Gesendet",
|
|
1931
|
+
"Gesendete Elemente"
|
|
1932
|
+
];
|
|
1933
|
+
async function resolveSentFolder(client) {
|
|
1934
|
+
const mailboxes = await client.list();
|
|
1935
|
+
for (const mb of mailboxes) {
|
|
1936
|
+
if (mb.specialUse === "\\Sent" || mb.specialUse === "Sent") {
|
|
1937
|
+
return mb.path;
|
|
1938
|
+
}
|
|
1939
|
+
if (mb.flags.has("\\Sent")) {
|
|
1940
|
+
return mb.path;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
for (const name of SENT_FALLBACKS) {
|
|
1944
|
+
try {
|
|
1945
|
+
const status = await client.status(name, { messages: true });
|
|
1946
|
+
if (status) return name;
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return void 0;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/tools/smtp/forward.ts
|
|
1954
|
+
var forward_default = defineTool({
|
|
1955
|
+
name: "smtp_forward",
|
|
1956
|
+
description: "Weiterleiten (Original quoted oder als Attachment) + Sent-Ablage",
|
|
1957
|
+
category: "smtp",
|
|
1958
|
+
inputSchema: z27.object({
|
|
1959
|
+
to: z27.union([z27.string(), z27.array(z27.object({ address: z27.string(), name: z27.string().optional() }))]).describe("Forward recipient(s)"),
|
|
1960
|
+
cc: z27.union([z27.string(), z27.array(z27.object({ address: z27.string(), name: z27.string().optional() }))]).optional().describe("CC recipient(s)"),
|
|
1961
|
+
bcc: z27.union([z27.string(), z27.array(z27.object({ address: z27.string(), name: z27.string().optional() }))]).optional().describe("BCC recipient(s)"),
|
|
1962
|
+
subject: z27.string().optional().describe("Forward subject (default: Fwd: <original subject>)"),
|
|
1963
|
+
text: z27.string().optional().describe("Additional plain text body"),
|
|
1964
|
+
html: z27.string().optional().describe("Additional HTML body"),
|
|
1965
|
+
originalMailbox: z27.string().min(1).describe("Mailbox containing the original message"),
|
|
1966
|
+
originalUid: z27.number().int().positive().describe("UID of the original message"),
|
|
1967
|
+
forwardAsAttachment: z27.boolean().default(false).describe("Forward as attachment (RFC-822) (default: false)"),
|
|
1968
|
+
account: z27.string().optional().describe("Account name (default: default_account)"),
|
|
1969
|
+
saveToSent: z27.boolean().default(true).describe("Save copy to Sent folder (default: true)")
|
|
1970
|
+
}),
|
|
1971
|
+
handler: async (input, ctx) => {
|
|
1972
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
1973
|
+
const transport = await ctx.smtp.acquire(accountName);
|
|
1974
|
+
const accConfig = ctx.config.accounts.get(accountName);
|
|
1975
|
+
const imapClient = await ctx.imap.acquire(accountName);
|
|
1976
|
+
const mb = await imapClient.mailboxOpen(input.originalMailbox);
|
|
1977
|
+
if (!mb) throw new MailboxNotFoundError(input.originalMailbox);
|
|
1978
|
+
const original = await imapClient.fetchOne(input.originalUid, {
|
|
1979
|
+
uid: true,
|
|
1980
|
+
envelope: true,
|
|
1981
|
+
source: true
|
|
1982
|
+
});
|
|
1983
|
+
if (!original) throw new UidNotFoundError(input.originalUid, input.originalMailbox);
|
|
1984
|
+
const enc = original.envelope;
|
|
1985
|
+
if (!enc) throw new Error("Original message has no envelope");
|
|
1986
|
+
const subject = input.subject ?? (enc.subject?.startsWith("Fwd:") ? enc.subject : `Fwd: ${enc.subject ?? ""}`);
|
|
1987
|
+
const mailOptions = {
|
|
1988
|
+
from: accConfig.from_name ? `"${accConfig.from_name}" <${accConfig.user}>` : accConfig.user,
|
|
1989
|
+
to: typeof input.to === "string" ? input.to : input.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
|
|
1990
|
+
cc: input.cc ? typeof input.cc === "string" ? input.cc : input.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ") : void 0,
|
|
1991
|
+
bcc: input.bcc ? typeof input.bcc === "string" ? input.bcc : input.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ") : void 0,
|
|
1992
|
+
subject
|
|
1993
|
+
};
|
|
1994
|
+
if (input.forwardAsAttachment) {
|
|
1995
|
+
const rawSource = original.source?.toString() ?? "";
|
|
1996
|
+
mailOptions.attachments = [
|
|
1997
|
+
{
|
|
1998
|
+
filename: `forwarded-${input.originalUid}.eml`,
|
|
1999
|
+
content: rawSource,
|
|
2000
|
+
contentType: "message/rfc822"
|
|
2001
|
+
}
|
|
2002
|
+
];
|
|
2003
|
+
mailOptions.text = input.text ?? "";
|
|
2004
|
+
mailOptions.html = input.html ?? "";
|
|
2005
|
+
} else {
|
|
2006
|
+
const fromStr = enc.from ? Array.isArray(enc.from) ? enc.from.map((f) => f.address ?? "").join(", ") : "" : "";
|
|
2007
|
+
const toStr = enc.to ? Array.isArray(enc.to) ? enc.to.map((t) => t.address ?? "").join(", ") : "" : "";
|
|
2008
|
+
const dateStr = enc.date ? enc.date.toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
2009
|
+
const quotedBody = original.source?.toString() ?? "";
|
|
2010
|
+
const quotedText = input.text ? `${input.text}
|
|
2011
|
+
|
|
2012
|
+
-------- Forwarded Message --------
|
|
2013
|
+
Subject: ${enc.subject ?? ""}
|
|
2014
|
+
Date: ${dateStr}
|
|
2015
|
+
From: ${fromStr}
|
|
2016
|
+
To: ${toStr}
|
|
2017
|
+
|
|
2018
|
+
${quotedBody}` : `-------- Forwarded Message --------
|
|
2019
|
+
Subject: ${enc.subject ?? ""}
|
|
2020
|
+
Date: ${dateStr}
|
|
2021
|
+
From: ${fromStr}
|
|
2022
|
+
To: ${toStr}
|
|
2023
|
+
|
|
2024
|
+
${quotedBody}`;
|
|
2025
|
+
mailOptions.text = quotedText;
|
|
2026
|
+
}
|
|
2027
|
+
let info;
|
|
2028
|
+
try {
|
|
2029
|
+
info = await transport.sendMail(mailOptions);
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
throw new SmtpRelayError(`Failed to forward email: ${err}`);
|
|
2032
|
+
}
|
|
2033
|
+
let savedToSent = false;
|
|
2034
|
+
let sentMailbox;
|
|
2035
|
+
let sentSaveError;
|
|
2036
|
+
if (input.saveToSent) {
|
|
2037
|
+
try {
|
|
2038
|
+
const sentFolder = await resolveSentFolder(imapClient);
|
|
2039
|
+
if (sentFolder) {
|
|
2040
|
+
sentMailbox = sentFolder;
|
|
2041
|
+
const rawLines = [];
|
|
2042
|
+
rawLines.push(`From: ${mailOptions.from}`);
|
|
2043
|
+
rawLines.push(`To: ${mailOptions.to}`);
|
|
2044
|
+
if (mailOptions.cc) rawLines.push(`Cc: ${mailOptions.cc}`);
|
|
2045
|
+
rawLines.push(`Subject: ${subject}`);
|
|
2046
|
+
rawLines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
2047
|
+
rawLines.push("MIME-Version: 1.0");
|
|
2048
|
+
rawLines.push("Content-Type: text/plain; charset=UTF-8");
|
|
2049
|
+
rawLines.push("");
|
|
2050
|
+
rawLines.push(mailOptions.text ?? "");
|
|
2051
|
+
await imapClient.append(sentFolder, rawLines.join("\r\n"));
|
|
2052
|
+
savedToSent = true;
|
|
2053
|
+
} else {
|
|
2054
|
+
sentSaveError = "No Sent folder found";
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
sentSaveError = `Failed to save to Sent: ${err}`;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return {
|
|
2061
|
+
messageId: info.messageId ?? "",
|
|
2062
|
+
accepted: info.accepted ?? [],
|
|
2063
|
+
rejected: info.rejected ?? [],
|
|
2064
|
+
response: info.response ?? "",
|
|
2065
|
+
savedToSent,
|
|
2066
|
+
...sentMailbox ? { sentMailbox } : {},
|
|
2067
|
+
...sentSaveError ? { sentSaveError } : {}
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2072
|
+
// src/tools/smtp/reply.ts
|
|
2073
|
+
import { z as z28 } from "zod";
|
|
2074
|
+
var reply_default = defineTool({
|
|
2075
|
+
name: "smtp_reply",
|
|
2076
|
+
description: "Antwort mit korrekter In-Reply-To/References-Kette + Sent-Ablage",
|
|
2077
|
+
category: "smtp",
|
|
2078
|
+
inputSchema: z28.object({
|
|
2079
|
+
to: z28.union([z28.string(), z28.array(z28.object({ address: z28.string(), name: z28.string().optional() }))]).optional().describe("Reply recipient(s) (default: from of original)"),
|
|
2080
|
+
cc: z28.union([z28.string(), z28.array(z28.object({ address: z28.string(), name: z28.string().optional() }))]).optional().describe("CC recipient(s)"),
|
|
2081
|
+
subject: z28.string().optional().describe("Reply subject (default: Re: <original subject>)"),
|
|
2082
|
+
text: z28.string().optional().describe("Plain text body"),
|
|
2083
|
+
html: z28.string().optional().describe("HTML body"),
|
|
2084
|
+
originalMailbox: z28.string().min(1).describe("Mailbox containing the original message"),
|
|
2085
|
+
originalUid: z28.number().int().positive().describe("UID of the original message"),
|
|
2086
|
+
replyAll: z28.boolean().default(false).describe("Reply to all recipients (default: false)"),
|
|
2087
|
+
account: z28.string().optional().describe("Account name (default: default_account)"),
|
|
2088
|
+
saveToSent: z28.boolean().default(true).describe("Save copy to Sent folder (default: true)")
|
|
2089
|
+
}),
|
|
2090
|
+
handler: async (input, ctx) => {
|
|
2091
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
2092
|
+
const transport = await ctx.smtp.acquire(accountName);
|
|
2093
|
+
const accConfig = ctx.config.accounts.get(accountName);
|
|
2094
|
+
const imapClient = await ctx.imap.acquire(accountName);
|
|
2095
|
+
const mb = await imapClient.mailboxOpen(input.originalMailbox);
|
|
2096
|
+
if (!mb) throw new MailboxNotFoundError(input.originalMailbox);
|
|
2097
|
+
const original = await imapClient.fetchOne(input.originalUid, {
|
|
2098
|
+
uid: true,
|
|
2099
|
+
envelope: true,
|
|
2100
|
+
headers: true
|
|
2101
|
+
});
|
|
2102
|
+
if (!original) throw new UidNotFoundError(input.originalUid, input.originalMailbox);
|
|
2103
|
+
const enc = original.envelope;
|
|
2104
|
+
if (!enc) throw new Error("Original message has no envelope");
|
|
2105
|
+
const messageId = enc.messageId ?? "";
|
|
2106
|
+
const rawHeaders = original.headers?.toString() ?? "";
|
|
2107
|
+
const refMatch = rawHeaders.match(/^references:\s*(.*)$/im);
|
|
2108
|
+
const existingRefs = refMatch ? refMatch[1]?.trim().split(/\s+/) ?? [] : [];
|
|
2109
|
+
const references = [...existingRefs, messageId].filter(Boolean);
|
|
2110
|
+
const inReplyTo = messageId;
|
|
2111
|
+
let to = input.to;
|
|
2112
|
+
const cc = input.cc;
|
|
2113
|
+
if (!to) {
|
|
2114
|
+
const fromAddr = enc.from?.[0];
|
|
2115
|
+
to = fromAddr?.address ?? "";
|
|
2116
|
+
}
|
|
2117
|
+
if (input.replyAll && enc.to) {
|
|
2118
|
+
}
|
|
2119
|
+
const subject = input.subject ?? (enc.subject?.startsWith("Re:") ? enc.subject : `Re: ${enc.subject ?? ""}`);
|
|
2120
|
+
const mailOptions = {
|
|
2121
|
+
from: accConfig.from_name ? `"${accConfig.from_name}" <${accConfig.user}>` : accConfig.user,
|
|
2122
|
+
to: typeof to === "string" ? to : Array.isArray(to) ? to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ") : void 0,
|
|
2123
|
+
cc: typeof cc === "string" ? cc : Array.isArray(cc) ? cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ") : void 0,
|
|
2124
|
+
subject,
|
|
2125
|
+
text: input.text,
|
|
2126
|
+
html: input.html,
|
|
2127
|
+
inReplyTo,
|
|
2128
|
+
references: references.join(" ")
|
|
2129
|
+
};
|
|
2130
|
+
let info;
|
|
2131
|
+
try {
|
|
2132
|
+
info = await transport.sendMail(mailOptions);
|
|
2133
|
+
} catch (err) {
|
|
2134
|
+
throw new SmtpRelayError(`Failed to send reply: ${err}`);
|
|
2135
|
+
}
|
|
2136
|
+
let savedToSent = false;
|
|
2137
|
+
let sentMailbox;
|
|
2138
|
+
let sentSaveError;
|
|
2139
|
+
if (input.saveToSent) {
|
|
2140
|
+
try {
|
|
2141
|
+
const sentFolder = await resolveSentFolder(imapClient);
|
|
2142
|
+
if (sentFolder) {
|
|
2143
|
+
sentMailbox = sentFolder;
|
|
2144
|
+
const rawLines = [];
|
|
2145
|
+
rawLines.push(`From: ${mailOptions.from}`);
|
|
2146
|
+
rawLines.push(`To: ${mailOptions.to}`);
|
|
2147
|
+
if (mailOptions.cc) rawLines.push(`Cc: ${mailOptions.cc}`);
|
|
2148
|
+
rawLines.push(`Subject: ${subject}`);
|
|
2149
|
+
rawLines.push(`In-Reply-To: ${inReplyTo}`);
|
|
2150
|
+
rawLines.push(`References: ${references.join(" ")}`);
|
|
2151
|
+
rawLines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
2152
|
+
rawLines.push("MIME-Version: 1.0");
|
|
2153
|
+
rawLines.push("Content-Type: text/plain; charset=UTF-8");
|
|
2154
|
+
rawLines.push("");
|
|
2155
|
+
rawLines.push(input.text ?? input.html ?? "");
|
|
2156
|
+
await imapClient.append(sentFolder, rawLines.join("\r\n"));
|
|
2157
|
+
savedToSent = true;
|
|
2158
|
+
} else {
|
|
2159
|
+
sentSaveError = "No Sent folder found";
|
|
2160
|
+
}
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
sentSaveError = `Failed to save to Sent: ${err}`;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return {
|
|
2166
|
+
messageId: info.messageId ?? "",
|
|
2167
|
+
inReplyTo,
|
|
2168
|
+
accepted: info.accepted ?? [],
|
|
2169
|
+
rejected: info.rejected ?? [],
|
|
2170
|
+
response: info.response ?? "",
|
|
2171
|
+
savedToSent,
|
|
2172
|
+
...sentMailbox ? { sentMailbox } : {},
|
|
2173
|
+
...sentSaveError ? { sentSaveError } : {}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
// src/tools/smtp/send-raw.ts
|
|
2179
|
+
import { z as z29 } from "zod";
|
|
2180
|
+
var send_raw_default = defineTool({
|
|
2181
|
+
name: "smtp_send_raw",
|
|
2182
|
+
description: "Vor-formatierte RFC-822 senden (Power-User) + Sent-Ablage",
|
|
2183
|
+
category: "smtp",
|
|
2184
|
+
inputSchema: z29.object({
|
|
2185
|
+
raw: z29.string().min(1).describe("Raw RFC-822 message content to send"),
|
|
2186
|
+
account: z29.string().optional().describe("Account name (default: default_account)"),
|
|
2187
|
+
saveToSent: z29.boolean().default(true).describe("Save copy to Sent folder (default: true)")
|
|
2188
|
+
}),
|
|
2189
|
+
handler: async (input, ctx) => {
|
|
2190
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
2191
|
+
const transport = await ctx.smtp.acquire(accountName);
|
|
2192
|
+
const mailOptions = {
|
|
2193
|
+
raw: input.raw
|
|
2194
|
+
};
|
|
2195
|
+
let info;
|
|
2196
|
+
try {
|
|
2197
|
+
info = await transport.sendMail(mailOptions);
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
throw new SmtpRelayError(`Failed to send raw email: ${err}`);
|
|
2200
|
+
}
|
|
2201
|
+
let savedToSent = false;
|
|
2202
|
+
let sentMailbox;
|
|
2203
|
+
let sentSaveError;
|
|
2204
|
+
if (input.saveToSent) {
|
|
2205
|
+
try {
|
|
2206
|
+
const imapClient = await ctx.imap.acquire(accountName);
|
|
2207
|
+
const sentFolder = await resolveSentFolder(imapClient);
|
|
2208
|
+
if (sentFolder) {
|
|
2209
|
+
sentMailbox = sentFolder;
|
|
2210
|
+
await imapClient.append(sentFolder, input.raw);
|
|
2211
|
+
savedToSent = true;
|
|
2212
|
+
} else {
|
|
2213
|
+
sentSaveError = "No Sent folder found";
|
|
2214
|
+
}
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
sentSaveError = `Failed to save to Sent: ${err}`;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return {
|
|
2220
|
+
messageId: info.messageId ?? "",
|
|
2221
|
+
accepted: info.accepted ?? [],
|
|
2222
|
+
rejected: info.rejected ?? [],
|
|
2223
|
+
response: info.response ?? "",
|
|
2224
|
+
savedToSent,
|
|
2225
|
+
...sentMailbox ? { sentMailbox } : {},
|
|
2226
|
+
...sentSaveError ? { sentSaveError } : {}
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
// src/tools/smtp/send.ts
|
|
2232
|
+
import { z as z30 } from "zod";
|
|
2233
|
+
function buildAddresses(list) {
|
|
2234
|
+
if (!list) return void 0;
|
|
2235
|
+
if (typeof list === "string") return list;
|
|
2236
|
+
return list.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address);
|
|
2237
|
+
}
|
|
2238
|
+
var send_default = defineTool({
|
|
2239
|
+
name: "smtp_send",
|
|
2240
|
+
description: "Mail senden + optionale Sent-Ablage",
|
|
2241
|
+
category: "smtp",
|
|
2242
|
+
inputSchema: z30.object({
|
|
2243
|
+
to: z30.union([z30.string(), z30.array(z30.object({ address: z30.string(), name: z30.string().optional() }))]).describe("Recipient(s)"),
|
|
2244
|
+
cc: z30.union([z30.string(), z30.array(z30.object({ address: z30.string(), name: z30.string().optional() }))]).optional().describe("CC recipient(s)"),
|
|
2245
|
+
bcc: z30.union([z30.string(), z30.array(z30.object({ address: z30.string(), name: z30.string().optional() }))]).optional().describe("BCC recipient(s)"),
|
|
2246
|
+
subject: z30.string().min(1).describe("Email subject"),
|
|
2247
|
+
text: z30.string().optional().describe("Plain text body"),
|
|
2248
|
+
html: z30.string().optional().describe("HTML body"),
|
|
2249
|
+
attachments: z30.array(
|
|
2250
|
+
z30.object({
|
|
2251
|
+
filename: z30.string().optional(),
|
|
2252
|
+
content: z30.string().optional(),
|
|
2253
|
+
path: z30.string().optional(),
|
|
2254
|
+
contentType: z30.string().optional(),
|
|
2255
|
+
cid: z30.string().optional()
|
|
2256
|
+
})
|
|
2257
|
+
).optional().describe("Attachments"),
|
|
2258
|
+
account: z30.string().optional().describe("Account name (default: default_account)"),
|
|
2259
|
+
saveToSent: z30.boolean().default(true).describe("Save copy to Sent folder (default: true)")
|
|
2260
|
+
}),
|
|
2261
|
+
handler: async (input, ctx) => {
|
|
2262
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
2263
|
+
const transport = await ctx.smtp.acquire(accountName);
|
|
2264
|
+
const accConfig = ctx.config.accounts.get(accountName);
|
|
2265
|
+
const mailOptions = {
|
|
2266
|
+
from: accConfig.from_name ? `"${accConfig.from_name}" <${accConfig.user}>` : accConfig.user,
|
|
2267
|
+
to: buildAddresses(input.to),
|
|
2268
|
+
cc: buildAddresses(
|
|
2269
|
+
input.cc
|
|
2270
|
+
),
|
|
2271
|
+
bcc: buildAddresses(
|
|
2272
|
+
input.bcc
|
|
2273
|
+
),
|
|
2274
|
+
subject: input.subject,
|
|
2275
|
+
text: input.text,
|
|
2276
|
+
html: input.html,
|
|
2277
|
+
attachments: input.attachments
|
|
2278
|
+
};
|
|
2279
|
+
let info;
|
|
2280
|
+
try {
|
|
2281
|
+
info = await transport.sendMail(mailOptions);
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
throw new SmtpRelayError(`Failed to send email: ${err}`);
|
|
2284
|
+
}
|
|
2285
|
+
let savedToSent = false;
|
|
2286
|
+
let sentMailbox;
|
|
2287
|
+
let sentSaveError;
|
|
2288
|
+
if (input.saveToSent) {
|
|
2289
|
+
try {
|
|
2290
|
+
const imapClient = await ctx.imap.acquire(accountName);
|
|
2291
|
+
const sentFolder = await resolveSentFolder(imapClient);
|
|
2292
|
+
if (sentFolder) {
|
|
2293
|
+
sentMailbox = sentFolder;
|
|
2294
|
+
const raw = await buildRfc822(
|
|
2295
|
+
{
|
|
2296
|
+
user: accConfig.user,
|
|
2297
|
+
...accConfig.from_name ? { from_name: accConfig.from_name } : {}
|
|
2298
|
+
},
|
|
2299
|
+
mailOptions
|
|
2300
|
+
);
|
|
2301
|
+
await imapClient.append(sentFolder, raw);
|
|
2302
|
+
savedToSent = true;
|
|
2303
|
+
} else {
|
|
2304
|
+
sentSaveError = "No Sent folder found";
|
|
2305
|
+
}
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
sentSaveError = `Failed to save to Sent: ${err}`;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return {
|
|
2311
|
+
messageId: info.messageId ?? "",
|
|
2312
|
+
accepted: info.accepted ?? [],
|
|
2313
|
+
rejected: info.rejected ?? [],
|
|
2314
|
+
response: info.response ?? "",
|
|
2315
|
+
savedToSent,
|
|
2316
|
+
...sentMailbox ? { sentMailbox } : {},
|
|
2317
|
+
...sentSaveError ? { sentSaveError } : {}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
async function buildRfc822(acc, opts) {
|
|
2322
|
+
const lines = [];
|
|
2323
|
+
const from = typeof opts.from === "string" ? opts.from : acc.from_name ? `"${acc.from_name}" <${acc.user}>` : acc.user;
|
|
2324
|
+
const to = typeof opts.to === "string" ? opts.to : Array.isArray(opts.to) ? opts.to.join(", ") : "";
|
|
2325
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
2326
|
+
lines.push(`From: ${from}`);
|
|
2327
|
+
lines.push(`To: ${to}`);
|
|
2328
|
+
if (opts.cc) {
|
|
2329
|
+
const ccStr = typeof opts.cc === "string" ? opts.cc : opts.cc.map((a) => a.address).join(", ");
|
|
2330
|
+
lines.push(`Cc: ${ccStr}`);
|
|
2331
|
+
}
|
|
2332
|
+
lines.push(`Subject: ${opts.subject ?? ""}`);
|
|
2333
|
+
lines.push(`Date: ${date}`);
|
|
2334
|
+
lines.push("MIME-Version: 1.0");
|
|
2335
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
2336
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
2337
|
+
lines.push("");
|
|
2338
|
+
lines.push(opts.text ? opts.text : opts.html ? opts.html : "");
|
|
2339
|
+
return lines.join("\r\n");
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/tools/smtp/verify-connection.ts
|
|
2343
|
+
import { z as z31 } from "zod";
|
|
2344
|
+
var verify_connection_default = defineTool({
|
|
2345
|
+
name: "smtp_verify_connection",
|
|
2346
|
+
description: "SMTP-Connection-Health-Check (EHLO, AUTH)",
|
|
2347
|
+
category: "smtp",
|
|
2348
|
+
inputSchema: z31.object({
|
|
2349
|
+
account: z31.string().optional().describe("Account name (default: default_account)")
|
|
2350
|
+
}),
|
|
2351
|
+
handler: async (input, ctx) => {
|
|
2352
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
2353
|
+
const transport = await ctx.smtp.acquire(accountName);
|
|
2354
|
+
const accConfig = ctx.config.accounts.get(accountName);
|
|
2355
|
+
const start = Date.now();
|
|
2356
|
+
try {
|
|
2357
|
+
const ok = await transport.verify();
|
|
2358
|
+
const latencyMs = Date.now() - start;
|
|
2359
|
+
return {
|
|
2360
|
+
ok,
|
|
2361
|
+
host: accConfig.smtp_host ?? "unknown",
|
|
2362
|
+
port: accConfig.smtp_port,
|
|
2363
|
+
tls: accConfig.smtp_tls === "implicit" ? "implicit" : accConfig.smtp_tls === "starttls" ? "starttls" : "none",
|
|
2364
|
+
latencyMs
|
|
2365
|
+
};
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
const error = err;
|
|
2368
|
+
if (error.message?.includes("auth") || error.message?.includes("login")) {
|
|
2369
|
+
throw new AuthError(`SMTP authentication failed: ${error.message}`);
|
|
2370
|
+
}
|
|
2371
|
+
throw new SmtpRelayError(`SMTP connection failed: ${error.message}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
// src/tools/account/add.ts
|
|
2377
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2378
|
+
import { parse as parseToml2, stringify as stringifyToml } from "smol-toml";
|
|
2379
|
+
import { z as z32 } from "zod";
|
|
2380
|
+
var add_default = defineTool({
|
|
2381
|
+
name: "account_add",
|
|
2382
|
+
description: "Neuen Account zur Config hinzuf\xFCgen",
|
|
2383
|
+
category: "account",
|
|
2384
|
+
inputSchema: z32.object({
|
|
2385
|
+
name: z32.string().min(1).describe("Account name"),
|
|
2386
|
+
user: z32.string().min(1).describe("Email address / IMAP login"),
|
|
2387
|
+
pass: z32.string().min(1).describe("Password or app password"),
|
|
2388
|
+
imap_host: z32.string().optional().describe("IMAP host (auto-detect if omitted)"),
|
|
2389
|
+
imap_port: z32.number().int().positive().optional().describe("IMAP port (default: 993)"),
|
|
2390
|
+
imap_tls: z32.enum(["implicit", "starttls", "none"]).optional().describe("IMAP TLS mode"),
|
|
2391
|
+
smtp_host: z32.string().optional().describe("SMTP host (auto-detect if omitted)"),
|
|
2392
|
+
smtp_port: z32.number().int().positive().optional().describe("SMTP port (default: 465)"),
|
|
2393
|
+
smtp_tls: z32.enum(["implicit", "starttls", "none"]).optional().describe("SMTP TLS mode"),
|
|
2394
|
+
from_name: z32.string().optional().describe("Display name for sent emails"),
|
|
2395
|
+
verify_tls: z32.boolean().optional().describe("Verify TLS certificate (default: true)")
|
|
2396
|
+
}),
|
|
2397
|
+
handler: async (input, ctx) => {
|
|
2398
|
+
if (ctx.config.mode !== "config-file" || !ctx.config.configPath) {
|
|
2399
|
+
throw new ConfigError(
|
|
2400
|
+
"Account mutations require a config file (multi-account mode). Use --config=<path> or set up a config file."
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
const configPath = ctx.config.configPath;
|
|
2404
|
+
let raw;
|
|
2405
|
+
try {
|
|
2406
|
+
raw = readFileSync2(configPath, "utf8");
|
|
2407
|
+
} catch {
|
|
2408
|
+
throw new ConfigError(`Cannot read config file: ${configPath}`);
|
|
2409
|
+
}
|
|
2410
|
+
const parsed = parseToml2(raw);
|
|
2411
|
+
const accounts = parsed.accounts ?? [];
|
|
2412
|
+
if (accounts.some((a) => a.name === input.name)) {
|
|
2413
|
+
throw new ConfigError(`Account "${input.name}" already exists`);
|
|
2414
|
+
}
|
|
2415
|
+
const newAccount = {
|
|
2416
|
+
name: input.name,
|
|
2417
|
+
user: input.user,
|
|
2418
|
+
pass: input.pass
|
|
2419
|
+
};
|
|
2420
|
+
if (input.imap_host) newAccount.imap_host = input.imap_host;
|
|
2421
|
+
if (input.imap_port) newAccount.imap_port = input.imap_port;
|
|
2422
|
+
if (input.imap_tls) newAccount.imap_tls = input.imap_tls;
|
|
2423
|
+
if (input.smtp_host) newAccount.smtp_host = input.smtp_host;
|
|
2424
|
+
if (input.smtp_port) newAccount.smtp_port = input.smtp_port;
|
|
2425
|
+
if (input.smtp_tls) newAccount.smtp_tls = input.smtp_tls;
|
|
2426
|
+
if (input.from_name) newAccount.from_name = input.from_name;
|
|
2427
|
+
if (input.verify_tls !== void 0) newAccount.verify_tls = input.verify_tls;
|
|
2428
|
+
accounts.push(newAccount);
|
|
2429
|
+
parsed.accounts = accounts;
|
|
2430
|
+
const tomlStr = stringifyToml(parsed);
|
|
2431
|
+
writeFileSync2(configPath, tomlStr, "utf8");
|
|
2432
|
+
ctx.logger.info({ account: input.name }, "Account added to config file");
|
|
2433
|
+
return { name: input.name, created: true, configPath };
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
|
|
2437
|
+
// src/tools/account/delete.ts
|
|
2438
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2439
|
+
import { parse as parseToml3, stringify as stringifyToml2 } from "smol-toml";
|
|
2440
|
+
import { z as z33 } from "zod";
|
|
2441
|
+
var delete_default = defineTool({
|
|
2442
|
+
name: "account_delete",
|
|
2443
|
+
description: "Account aus Config entfernen",
|
|
2444
|
+
category: "account",
|
|
2445
|
+
inputSchema: z33.object({
|
|
2446
|
+
name: z33.string().min(1).describe("Account name to delete")
|
|
2447
|
+
}),
|
|
2448
|
+
handler: async (input, ctx) => {
|
|
2449
|
+
if (ctx.config.mode !== "config-file" || !ctx.config.configPath) {
|
|
2450
|
+
throw new ConfigError("Account mutations require a config file (multi-account mode).");
|
|
2451
|
+
}
|
|
2452
|
+
const configPath = ctx.config.configPath;
|
|
2453
|
+
let raw;
|
|
2454
|
+
try {
|
|
2455
|
+
raw = readFileSync3(configPath, "utf8");
|
|
2456
|
+
} catch {
|
|
2457
|
+
throw new ConfigError(`Cannot read config file: ${configPath}`);
|
|
2458
|
+
}
|
|
2459
|
+
const parsed = parseToml3(raw);
|
|
2460
|
+
const accounts = parsed.accounts ?? [];
|
|
2461
|
+
const idx = accounts.findIndex((a) => a.name === input.name);
|
|
2462
|
+
if (idx === -1) {
|
|
2463
|
+
throw new AccountNotFoundError(input.name);
|
|
2464
|
+
}
|
|
2465
|
+
accounts.splice(idx, 1);
|
|
2466
|
+
parsed.accounts = accounts;
|
|
2467
|
+
if (parsed.default_account === input.name) {
|
|
2468
|
+
if (accounts.length > 0) {
|
|
2469
|
+
parsed.default_account = accounts[0]?.name;
|
|
2470
|
+
} else {
|
|
2471
|
+
parsed.default_account = void 0;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
const tomlStr = stringifyToml2(parsed);
|
|
2475
|
+
writeFileSync3(configPath, tomlStr, "utf8");
|
|
2476
|
+
ctx.logger.info({ account: input.name }, "Account deleted");
|
|
2477
|
+
return { name: input.name, deleted: true };
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
// src/tools/account/list.ts
|
|
2482
|
+
import { z as z34 } from "zod";
|
|
2483
|
+
var list_default = defineTool({
|
|
2484
|
+
name: "account_list",
|
|
2485
|
+
description: "Konfigurierte Accounts auflisten (Credentials masked)",
|
|
2486
|
+
category: "account",
|
|
2487
|
+
inputSchema: z34.object({}),
|
|
2488
|
+
handler: async (_input, ctx) => {
|
|
2489
|
+
const accounts = [...ctx.config.accounts.entries()].map(([name, acc]) => ({
|
|
2490
|
+
name,
|
|
2491
|
+
user: acc.user,
|
|
2492
|
+
imapHost: acc.imap_host ?? "auto-detect",
|
|
2493
|
+
smtpHost: acc.smtp_host ?? "auto-detect"
|
|
2494
|
+
}));
|
|
2495
|
+
return {
|
|
2496
|
+
defaultAccount: ctx.config.defaultAccount,
|
|
2497
|
+
accounts,
|
|
2498
|
+
mode: ctx.config.mode
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2503
|
+
// src/tools/account/update.ts
|
|
2504
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2505
|
+
import { parse as parseToml4, stringify as stringifyToml3 } from "smol-toml";
|
|
2506
|
+
import { z as z35 } from "zod";
|
|
2507
|
+
var update_default = defineTool({
|
|
2508
|
+
name: "account_update",
|
|
2509
|
+
description: "Bestehenden Account modifizieren",
|
|
2510
|
+
category: "account",
|
|
2511
|
+
inputSchema: z35.object({
|
|
2512
|
+
name: z35.string().min(1).describe("Account name to modify"),
|
|
2513
|
+
user: z35.string().optional().describe("New email address / IMAP login"),
|
|
2514
|
+
pass: z35.string().optional().describe("New password or app password"),
|
|
2515
|
+
imap_host: z35.string().optional().describe("New IMAP host"),
|
|
2516
|
+
imap_port: z35.number().int().positive().optional().describe("New IMAP port"),
|
|
2517
|
+
imap_tls: z35.enum(["implicit", "starttls", "none"]).optional().describe("New IMAP TLS mode"),
|
|
2518
|
+
smtp_host: z35.string().optional().describe("New SMTP host"),
|
|
2519
|
+
smtp_port: z35.number().int().positive().optional().describe("New SMTP port"),
|
|
2520
|
+
smtp_tls: z35.enum(["implicit", "starttls", "none"]).optional().describe("New SMTP TLS mode"),
|
|
2521
|
+
from_name: z35.string().optional().describe("New display name"),
|
|
2522
|
+
verify_tls: z35.boolean().optional().describe("New TLS verification setting")
|
|
2523
|
+
}),
|
|
2524
|
+
handler: async (input, ctx) => {
|
|
2525
|
+
if (ctx.config.mode !== "config-file" || !ctx.config.configPath) {
|
|
2526
|
+
throw new ConfigError("Account mutations require a config file (multi-account mode).");
|
|
2527
|
+
}
|
|
2528
|
+
const configPath = ctx.config.configPath;
|
|
2529
|
+
let raw;
|
|
2530
|
+
try {
|
|
2531
|
+
raw = readFileSync4(configPath, "utf8");
|
|
2532
|
+
} catch {
|
|
2533
|
+
throw new ConfigError(`Cannot read config file: ${configPath}`);
|
|
2534
|
+
}
|
|
2535
|
+
const parsed = parseToml4(raw);
|
|
2536
|
+
const accounts = parsed.accounts ?? [];
|
|
2537
|
+
const account = accounts.find((a) => a.name === input.name);
|
|
2538
|
+
if (!account) {
|
|
2539
|
+
throw new AccountNotFoundError(input.name);
|
|
2540
|
+
}
|
|
2541
|
+
const changedFields = [];
|
|
2542
|
+
const updatable = [
|
|
2543
|
+
["user", input.user],
|
|
2544
|
+
["pass", input.pass],
|
|
2545
|
+
["imap_host", input.imap_host],
|
|
2546
|
+
["imap_port", input.imap_port],
|
|
2547
|
+
["imap_tls", input.imap_tls],
|
|
2548
|
+
["smtp_host", input.smtp_host],
|
|
2549
|
+
["smtp_port", input.smtp_port],
|
|
2550
|
+
["smtp_tls", input.smtp_tls],
|
|
2551
|
+
["from_name", input.from_name]
|
|
2552
|
+
];
|
|
2553
|
+
for (const [key, value] of updatable) {
|
|
2554
|
+
if (value !== void 0) {
|
|
2555
|
+
account[key] = value;
|
|
2556
|
+
changedFields.push(key);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (input.verify_tls !== void 0) {
|
|
2560
|
+
account.verify_tls = input.verify_tls;
|
|
2561
|
+
changedFields.push("verify_tls");
|
|
2562
|
+
}
|
|
2563
|
+
if (changedFields.length === 0) {
|
|
2564
|
+
throw new ConfigError("No fields to update");
|
|
2565
|
+
}
|
|
2566
|
+
parsed.accounts = accounts;
|
|
2567
|
+
const tomlStr = stringifyToml3(parsed);
|
|
2568
|
+
writeFileSync4(configPath, tomlStr, "utf8");
|
|
2569
|
+
ctx.logger.info({ account: input.name, changedFields }, "Account updated");
|
|
2570
|
+
return { name: input.name, updated: true, changedFields };
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
// src/tools/meta/health.ts
|
|
2575
|
+
import { z as z36 } from "zod";
|
|
2576
|
+
var health_default = defineTool({
|
|
2577
|
+
name: "meta_health",
|
|
2578
|
+
description: "IMAP + SMTP Erreichbarkeit, Latenz, Capabilities",
|
|
2579
|
+
category: "meta",
|
|
2580
|
+
inputSchema: z36.object({
|
|
2581
|
+
account: z36.string().optional().describe("Account name (default: default_account)")
|
|
2582
|
+
}),
|
|
2583
|
+
handler: async (input, ctx) => {
|
|
2584
|
+
const accountName = ctx.resolveAccount(input.account);
|
|
2585
|
+
const accConfig = ctx.config.accounts.get(accountName);
|
|
2586
|
+
let imapOk = false;
|
|
2587
|
+
let imapLatencyMs;
|
|
2588
|
+
let imapCaps;
|
|
2589
|
+
let imapError;
|
|
2590
|
+
try {
|
|
2591
|
+
const imapStart = Date.now();
|
|
2592
|
+
const client = await ctx.imap.acquire(accountName);
|
|
2593
|
+
const caps = [...client.capabilities.keys()];
|
|
2594
|
+
imapLatencyMs = Date.now() - imapStart;
|
|
2595
|
+
imapOk = true;
|
|
2596
|
+
imapCaps = caps;
|
|
2597
|
+
} catch (err) {
|
|
2598
|
+
imapError = String(err);
|
|
2599
|
+
}
|
|
2600
|
+
let smtpOk = false;
|
|
2601
|
+
let smtpLatencyMs;
|
|
2602
|
+
let smtpError;
|
|
2603
|
+
try {
|
|
2604
|
+
const smtpStart = Date.now();
|
|
2605
|
+
const transport = await ctx.smtp.acquire(accountName);
|
|
2606
|
+
const verify = await transport.verify();
|
|
2607
|
+
smtpLatencyMs = Date.now() - smtpStart;
|
|
2608
|
+
smtpOk = verify;
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
smtpError = String(err);
|
|
2611
|
+
}
|
|
2612
|
+
return {
|
|
2613
|
+
account: accountName,
|
|
2614
|
+
imap: {
|
|
2615
|
+
ok: imapOk,
|
|
2616
|
+
...imapLatencyMs !== void 0 ? { latencyMs: imapLatencyMs } : {},
|
|
2617
|
+
...imapCaps ? { capabilities: imapCaps } : {},
|
|
2618
|
+
...imapError ? { error: imapError } : {}
|
|
2619
|
+
},
|
|
2620
|
+
smtp: {
|
|
2621
|
+
ok: smtpOk,
|
|
2622
|
+
...smtpLatencyMs !== void 0 ? { latencyMs: smtpLatencyMs } : {},
|
|
2623
|
+
...smtpError ? { error: smtpError } : {}
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
// src/tools/meta/server-info.ts
|
|
2630
|
+
import { z as z37 } from "zod";
|
|
2631
|
+
var server_info_default = defineTool({
|
|
2632
|
+
name: "meta_server_info",
|
|
2633
|
+
description: "Aktive Tools, aktiver Modus, Version",
|
|
2634
|
+
category: "meta",
|
|
2635
|
+
inputSchema: z37.object({}),
|
|
2636
|
+
handler: async (_input, ctx) => {
|
|
2637
|
+
const flags = {
|
|
2638
|
+
safe: ctx.flags.safe,
|
|
2639
|
+
readonly: ctx.flags.readonly,
|
|
2640
|
+
noImap: ctx.flags.noImap,
|
|
2641
|
+
noSmtp: ctx.flags.noSmtp
|
|
2642
|
+
};
|
|
2643
|
+
if (ctx.flags.allowTools?.length) flags.allowTools = ctx.flags.allowTools;
|
|
2644
|
+
if (ctx.flags.denyTools?.length) flags.denyTools = ctx.flags.denyTools;
|
|
2645
|
+
return {
|
|
2646
|
+
name: SERVER_NAME,
|
|
2647
|
+
version: SERVER_VERSION,
|
|
2648
|
+
activeTools: ctx.activeTools,
|
|
2649
|
+
flags
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
// src/tools/index.ts
|
|
2655
|
+
var ALL_TOOLS = [
|
|
2656
|
+
list_mailboxes_default,
|
|
2657
|
+
status_mailbox_default,
|
|
2658
|
+
list_messages_default,
|
|
2659
|
+
get_message_default,
|
|
2660
|
+
get_message_headers_default,
|
|
2661
|
+
get_message_raw_default,
|
|
2662
|
+
get_messages_bulk_default,
|
|
2663
|
+
search_default,
|
|
2664
|
+
download_attachment_default,
|
|
2665
|
+
get_thread_default,
|
|
2666
|
+
get_quota_default,
|
|
2667
|
+
check_capabilities_default,
|
|
2668
|
+
mark_message_default,
|
|
2669
|
+
bulk_mark_default,
|
|
2670
|
+
move_message_default,
|
|
2671
|
+
copy_message_default,
|
|
2672
|
+
bulk_move_default,
|
|
2673
|
+
append_message_default,
|
|
2674
|
+
expunge_default,
|
|
2675
|
+
delete_message_default,
|
|
2676
|
+
create_mailbox_default,
|
|
2677
|
+
delete_mailbox_default,
|
|
2678
|
+
rename_mailbox_default,
|
|
2679
|
+
subscribe_mailbox_default,
|
|
2680
|
+
unsubscribe_mailbox_default,
|
|
2681
|
+
send_default,
|
|
2682
|
+
reply_default,
|
|
2683
|
+
forward_default,
|
|
2684
|
+
verify_connection_default,
|
|
2685
|
+
send_raw_default,
|
|
2686
|
+
list_default,
|
|
2687
|
+
add_default,
|
|
2688
|
+
update_default,
|
|
2689
|
+
delete_default,
|
|
2690
|
+
health_default,
|
|
2691
|
+
server_info_default
|
|
2692
|
+
];
|
|
2693
|
+
|
|
2694
|
+
// src/server/server.ts
|
|
2695
|
+
var SERVER_NAME = "classic-imap-smtp-mcp";
|
|
2696
|
+
var SERVER_VERSION = "0.3.0";
|
|
2697
|
+
function buildServer(opts, ctx, logger) {
|
|
2698
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
2699
|
+
const active = resolveActiveTools(ALL_TOOLS, opts);
|
|
2700
|
+
logger.info({ count: active.length, tools: active.map((t) => t.name) }, "Registering tools");
|
|
2701
|
+
for (const tool of active) {
|
|
2702
|
+
server.registerTool(
|
|
2703
|
+
tool.name,
|
|
2704
|
+
{ description: tool.description, inputSchema: tool.inputSchema },
|
|
2705
|
+
async (input) => {
|
|
2706
|
+
try {
|
|
2707
|
+
const result = await tool.handler(input, ctx);
|
|
2708
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2709
|
+
} catch (err) {
|
|
2710
|
+
const payload = err instanceof McpMailError ? err.toResult() : { code: "INTERNAL_ERROR", message: String(err) };
|
|
2711
|
+
logger.error({ tool: tool.name, err: payload }, "Tool call failed");
|
|
2712
|
+
return {
|
|
2713
|
+
isError: true,
|
|
2714
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
return server;
|
|
2721
|
+
}
|
|
2722
|
+
async function runStdio(server) {
|
|
2723
|
+
const transport = new StdioServerTransport();
|
|
2724
|
+
await server.connect(transport);
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// src/bin/main.ts
|
|
2728
|
+
var HELP = `classic-imap-smtp-mcp \u2014 classic IMAP/SMTP MCP server (stdio)
|
|
2729
|
+
|
|
2730
|
+
Usage: classic-imap-smtp-mcp [options]
|
|
2731
|
+
|
|
2732
|
+
Options:
|
|
2733
|
+
--safe Disable delete tools (delete/expunge/delete-mailbox)
|
|
2734
|
+
--readonly Read-only: no writes, no SMTP send
|
|
2735
|
+
--no-imap Disable all IMAP tools
|
|
2736
|
+
--no-smtp Disable all SMTP tools
|
|
2737
|
+
--allow-tools=<csv> Explicitly enable tools (overrides feature flags, prefix wildcards)
|
|
2738
|
+
--deny-tools=<csv> Explicitly remove tools (wins over everything, prefix wildcards)
|
|
2739
|
+
--account=<name> Default account override
|
|
2740
|
+
--config=<path> Alternative config path
|
|
2741
|
+
--log-level=<level> trace|debug|info|warn|error (default: info)
|
|
2742
|
+
--log-format=<fmt> json|pretty (default: json)
|
|
2743
|
+
-h, --help Show help
|
|
2744
|
+
-V, --version Show version
|
|
2745
|
+
|
|
2746
|
+
Subcommands:
|
|
2747
|
+
init Write a template config to the XDG path
|
|
2748
|
+
test [account] Test IMAP+SMTP connection
|
|
2749
|
+
list-tools Dry-run: which tools would register with the current flags
|
|
2750
|
+
`;
|
|
2751
|
+
var TEMPLATE_CONFIG = `# classic-imap-smtp-mcp \u2014 Multi-Account Config
|
|
2752
|
+
# Generated by \`classic-imap-smtp-mcp init\`
|
|
2753
|
+
# Pfad: see defaultConfigPath()
|
|
2754
|
+
# Datei sollte Permission 0600 haben \u2014 der Server warnt sonst.
|
|
2755
|
+
|
|
2756
|
+
default_account = "personal"
|
|
2757
|
+
|
|
2758
|
+
# Optional: globale Rate-Limits
|
|
2759
|
+
[limits]
|
|
2760
|
+
smtp_per_minute = 10
|
|
2761
|
+
imap_ops_per_second = 100
|
|
2762
|
+
|
|
2763
|
+
# --- F\xFCge hier deine Accounts ein ---
|
|
2764
|
+
# Beispiel mit Provider-Auto-Detect (nur user/pass n\xF6tig):
|
|
2765
|
+
[[accounts]]
|
|
2766
|
+
name = "personal"
|
|
2767
|
+
user = "you@gmail.com"
|
|
2768
|
+
pass = "your-app-password"
|
|
2769
|
+
from_name = "Your Name"
|
|
2770
|
+
|
|
2771
|
+
# Beispiel mit explizitem Host/Port:
|
|
2772
|
+
# [[accounts]]
|
|
2773
|
+
# name = "work"
|
|
2774
|
+
# user = "you@company.com"
|
|
2775
|
+
# pass = "another-app-password"
|
|
2776
|
+
# imap_host = "imap.company.com"
|
|
2777
|
+
# imap_port = 993
|
|
2778
|
+
# imap_tls = "implicit"
|
|
2779
|
+
# smtp_host = "smtp.company.com"
|
|
2780
|
+
# smtp_port = 587
|
|
2781
|
+
# smtp_tls = "starttls"
|
|
2782
|
+
# from_name = "You at Work"
|
|
2783
|
+
`;
|
|
2784
|
+
async function main() {
|
|
2785
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
2786
|
+
if (parsed.help) {
|
|
2787
|
+
process.stdout.write(HELP);
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
if (parsed.version) {
|
|
2791
|
+
process.stdout.write(`${SERVER_VERSION}
|
|
2792
|
+
`);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const logger = createLogger({ level: parsed.options.logLevel, format: parsed.options.logFormat });
|
|
2796
|
+
if (parsed.options.noImap && parsed.options.noSmtp) {
|
|
2797
|
+
logger.error("Both --no-imap and --no-smtp set: server would expose no mail tools. Aborting.");
|
|
2798
|
+
process.exit(1);
|
|
2799
|
+
}
|
|
2800
|
+
if (parsed.subcommand === "init") {
|
|
2801
|
+
await handleInit(logger);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
if (parsed.subcommand === "list-tools") {
|
|
2805
|
+
await handleListTools(parsed.options, logger);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (parsed.subcommand === "test") {
|
|
2809
|
+
await handleTest(parsed.options, parsed.subcommandArg, logger);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
const config = loadConfig(parsed.options.configPath);
|
|
2813
|
+
const imap = new ImapPool(config, logger);
|
|
2814
|
+
const smtp = new SmtpPool(config, logger);
|
|
2815
|
+
const activeTools = resolveActiveTools(ALL_TOOLS, parsed.options).map((t) => t.name);
|
|
2816
|
+
const ctx = {
|
|
2817
|
+
config,
|
|
2818
|
+
imap,
|
|
2819
|
+
smtp,
|
|
2820
|
+
logger,
|
|
2821
|
+
activeTools,
|
|
2822
|
+
flags: parsed.options,
|
|
2823
|
+
resolveAccount(name) {
|
|
2824
|
+
const target = name ?? parsed.options.account ?? config.defaultAccount;
|
|
2825
|
+
if (!config.accounts.has(target)) throw new AccountNotFoundError(target);
|
|
2826
|
+
return target;
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
const server = buildServer(parsed.options, ctx, logger);
|
|
2830
|
+
const shutdown = async () => {
|
|
2831
|
+
await Promise.allSettled([imap.closeAll(), smtp.closeAll()]);
|
|
2832
|
+
process.exit(0);
|
|
2833
|
+
};
|
|
2834
|
+
process.on("SIGINT", shutdown);
|
|
2835
|
+
process.on("SIGTERM", shutdown);
|
|
2836
|
+
await runStdio(server);
|
|
2837
|
+
logger.info("classic-imap-smtp-mcp running on stdio");
|
|
2838
|
+
}
|
|
2839
|
+
async function handleInit(logger) {
|
|
2840
|
+
const configPath = defaultConfigPath();
|
|
2841
|
+
const dir = dirname(configPath);
|
|
2842
|
+
if (!existsSync(configPath)) {
|
|
2843
|
+
mkdirSync(dir, { recursive: true });
|
|
2844
|
+
writeFileSync5(configPath, TEMPLATE_CONFIG, "utf8");
|
|
2845
|
+
try {
|
|
2846
|
+
if (process.platform !== "win32") {
|
|
2847
|
+
const { chmodSync } = await import("fs");
|
|
2848
|
+
chmodSync(configPath, 384);
|
|
2849
|
+
}
|
|
2850
|
+
} catch {
|
|
2851
|
+
}
|
|
2852
|
+
process.stdout.write(`Template config written to: ${configPath}
|
|
2853
|
+
`);
|
|
2854
|
+
process.stdout.write("Edit the file with your account details, then run the server.\n");
|
|
2855
|
+
} else {
|
|
2856
|
+
process.stdout.write(`Config file already exists: ${configPath}
|
|
2857
|
+
`);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
async function handleListTools(options, logger) {
|
|
2861
|
+
const active = resolveActiveTools(ALL_TOOLS, options);
|
|
2862
|
+
const lines = [];
|
|
2863
|
+
lines.push(`Active tools: ${active.length}/${ALL_TOOLS.length}`);
|
|
2864
|
+
for (const t of active) {
|
|
2865
|
+
lines.push(` ${t.name.padEnd(32)} ${t.category}`);
|
|
2866
|
+
}
|
|
2867
|
+
const denied = ALL_TOOLS.length - active.length;
|
|
2868
|
+
if (denied > 0) {
|
|
2869
|
+
lines.push(`
|
|
2870
|
+
Blocked by flags: ${denied} tool(s)`);
|
|
2871
|
+
const blocked = ALL_TOOLS.filter((t) => !active.find((a) => a.name === t.name));
|
|
2872
|
+
for (const t of blocked) {
|
|
2873
|
+
lines.push(` ${t.name.padEnd(32)} ${t.category}`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
process.stdout.write(`${lines.join("\n")}
|
|
2877
|
+
`);
|
|
2878
|
+
}
|
|
2879
|
+
async function handleTest(options, accountArg, logger) {
|
|
2880
|
+
const config = loadConfig(options.configPath);
|
|
2881
|
+
const imap = new ImapPool(config, logger);
|
|
2882
|
+
const smtp = new SmtpPool(config, logger);
|
|
2883
|
+
const accountName = accountArg ?? config.defaultAccount;
|
|
2884
|
+
if (!config.accounts.has(accountName)) {
|
|
2885
|
+
process.stderr.write(`Account not found: ${accountName}
|
|
2886
|
+
`);
|
|
2887
|
+
process.exit(1);
|
|
2888
|
+
}
|
|
2889
|
+
process.stdout.write(`Testing account: ${accountName}
|
|
2890
|
+
`);
|
|
2891
|
+
process.stdout.write(" IMAP... ");
|
|
2892
|
+
try {
|
|
2893
|
+
const start = Date.now();
|
|
2894
|
+
const client = await imap.acquire(accountName);
|
|
2895
|
+
const caps = [...client.capabilities.keys()];
|
|
2896
|
+
const latency = Date.now() - start;
|
|
2897
|
+
process.stdout.write(`OK (${latency}ms, ${caps.length} capabilities)
|
|
2898
|
+
`);
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
process.stdout.write(`FAIL: ${err}
|
|
2901
|
+
`);
|
|
2902
|
+
}
|
|
2903
|
+
process.stdout.write(" SMTP... ");
|
|
2904
|
+
try {
|
|
2905
|
+
const start = Date.now();
|
|
2906
|
+
const transport = await smtp.acquire(accountName);
|
|
2907
|
+
await transport.verify();
|
|
2908
|
+
const latency = Date.now() - start;
|
|
2909
|
+
process.stdout.write(`OK (${latency}ms)
|
|
2910
|
+
`);
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
process.stdout.write(`FAIL: ${err}
|
|
2913
|
+
`);
|
|
2914
|
+
}
|
|
2915
|
+
await imap.closeAll();
|
|
2916
|
+
await smtp.closeAll();
|
|
2917
|
+
}
|
|
2918
|
+
main().catch((err) => {
|
|
2919
|
+
const msg = err instanceof McpMailError ? `${err.code}: ${err.message}` : String(err);
|
|
2920
|
+
process.stderr.write(`Fatal: ${msg}
|
|
2921
|
+
`);
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
});
|
|
2924
|
+
//# sourceMappingURL=main.js.map
|