@rubytech/create-maxy-code 0.1.358 → 0.1.360
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/dist/__tests__/neo4j-pidfile-scrub.test.js +103 -0
- package/dist/index.js +42 -7
- package/dist/neo4j-pidfile-scrub.js +90 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
- package/payload/platform/plugins/browser/PLUGIN.md +1 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.js +58 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-status.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-probe.test.js +109 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-probe.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/source-fetch-efficiency.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/source-fetch-efficiency.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/source-fetch-efficiency.test.js +142 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/source-fetch-efficiency.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts +13 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +35 -0
- package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +18 -5
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +199 -114
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.js +33 -0
- package/payload/platform/plugins/email/mcp/dist/lib/setup-probe.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +11 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-status.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-status.js +24 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-status.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +9 -2
- package/payload/platform/scripts/__tests__/vnc-cdp-liveness.test.sh +222 -0
- package/payload/platform/scripts/rss-sampler.sh +16 -0
- package/payload/platform/scripts/vnc.sh +117 -3
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +11 -4
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +22 -7
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +0 -4
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +0 -15
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-Ro_RUTDh.js → AdminLoginScreens-FufN0KIm.js} +1 -1
- package/payload/server/public/assets/AdminShell-CmFCbj38.js +1 -0
- package/payload/server/public/assets/{Checkbox-dYLaMEdh.js → Checkbox-njajXf-b.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-B9Vx84BF.js +9 -0
- package/payload/server/public/assets/{OperatorConversations-2zJh7qnO.css → OperatorConversations-BqudLZH5.css} +1 -1
- package/payload/server/public/assets/admin-DYOYaRcc.js +1 -0
- package/payload/server/public/assets/{browser-DrXZ4wpy.js → browser-CUwGN38H.js} +1 -1
- package/payload/server/public/assets/calendar-HonT_eBL.js +1 -0
- package/payload/server/public/assets/chat-B-sCk0kO.js +1 -0
- package/payload/server/public/assets/data-vEYM-aPa.js +1 -0
- package/payload/server/public/assets/{graph-l_3_j3I9.js → graph-BLUtXXda.js} +3 -3
- package/payload/server/public/assets/{graph-labels-CJj55nHk.js → graph-labels-tUzvK8mv.js} +1 -1
- package/payload/server/public/assets/operator-BLEBOkrQ.js +1 -0
- package/payload/server/public/assets/page-CcnJY-yB.js +32 -0
- package/payload/server/public/assets/{public-smrhkkxD.js → public-B8GW-fZg.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-Dd8aAvU7.js → rotate-ccw-BA0MN5YR.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +6 -6
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +7 -7
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +8 -8
- package/payload/server/public/public.html +6 -6
- package/payload/server/public/assets/AdminShell-CmqhpAPU.js +0 -1
- package/payload/server/public/assets/OperatorConversations-Cei12sSK.js +0 -9
- package/payload/server/public/assets/admin-BldZQrJF.js +0 -1
- package/payload/server/public/assets/calendar-CQ4TCbO7.js +0 -1
- package/payload/server/public/assets/chat-D7w6_NE1.js +0 -1
- package/payload/server/public/assets/data-Dc5XNWgF.js +0 -1
- package/payload/server/public/assets/operator-CKmOgXNa.js +0 -1
- package/payload/server/public/assets/page-hG7xTrcO.js +0 -32
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ImapFlow } from "imapflow";
|
|
2
2
|
import { simpleParser } from "mailparser";
|
|
3
|
-
import { trackProbe, releaseProbe, newProbeToken } from "./setup-probe.js";
|
|
3
|
+
import { trackProbe, releaseProbe, newProbeToken, containProbeSocket } from "./setup-probe.js";
|
|
4
4
|
const CONNECT_TIMEOUT = 10_000;
|
|
5
5
|
const COMMAND_TIMEOUT = 30_000;
|
|
6
6
|
/** Diagnostic logging to stderr (captured by claude-agent-stderr log on Pi) */
|
|
@@ -299,42 +299,100 @@ export async function fetchAttachmentBytesForMessages(config, password, requests
|
|
|
299
299
|
}
|
|
300
300
|
return results;
|
|
301
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Classify an IMAP probe error by its structured fields, falling back to message
|
|
304
|
+
* strings only where no structured field carries the signal.
|
|
305
|
+
*
|
|
306
|
+
* The auth case is why this exists: ImapFlow reports an authentication rejection
|
|
307
|
+
* as a generic `Error("Command failed")` and carries the real signal in
|
|
308
|
+
* `authenticationFailed` / `serverResponseCode` / `responseText` (set by
|
|
309
|
+
* imapflow's login.js / authenticate.js), none of which appear in `err.message`.
|
|
310
|
+
* The transport cases (refused/not-found/timeout) carry `err.code` from Node's
|
|
311
|
+
* socket layer; TLS errors carry a cert/TLS/SSL marker in code or message.
|
|
312
|
+
*/
|
|
313
|
+
export function classifyImapError(err) {
|
|
314
|
+
const e = (err ?? {});
|
|
315
|
+
const message = typeof e.message === "string" ? e.message : String(err);
|
|
316
|
+
const code = typeof e.code === "string" ? e.code : "";
|
|
317
|
+
const serverCode = typeof e.serverResponseCode === "string" ? e.serverResponseCode : "";
|
|
318
|
+
const responseText = typeof e.responseText === "string" ? e.responseText : "";
|
|
319
|
+
// Order: structured/code signals beat fuzzy message strings. Auth's structured
|
|
320
|
+
// fields are unambiguous and come first. The code-bearing transport cases are
|
|
321
|
+
// checked before the message-only TLS and auth fallbacks, so a timeout/refusal
|
|
322
|
+
// whose OS message embeds a hostname containing "ssl"/"tls"/"authentication"
|
|
323
|
+
// is not stolen by a later substring match.
|
|
324
|
+
if (e.authenticationFailed === true || serverCode === "AUTHENTICATIONFAILED")
|
|
325
|
+
return "auth";
|
|
326
|
+
if (code === "ECONNREFUSED" || message.includes("ECONNREFUSED"))
|
|
327
|
+
return "refused";
|
|
328
|
+
if (code === "ENOTFOUND" || message.includes("ENOTFOUND"))
|
|
329
|
+
return "not-found";
|
|
330
|
+
if (code === "ETIMEDOUT" || code === "CONNECT_TIMEOUT" || code === "GREETING_TIMEOUT")
|
|
331
|
+
return "timeout";
|
|
332
|
+
// TLS errors carry a cert/TLS/SSL marker in the code (e.g. CERT_HAS_EXPIRED,
|
|
333
|
+
// ERR_TLS_*) or the message; checked on code first, message second.
|
|
334
|
+
if (/cert|TLS|SSL/i.test(`${code} ${message}`))
|
|
335
|
+
return "tls";
|
|
336
|
+
// Message-only fallbacks for errors that arrived without a structured field —
|
|
337
|
+
// last resort, after every code-based case above.
|
|
338
|
+
if (/AUTHENTICATIONFAILED|Invalid credentials|LOGIN failed|Authentication failed/i.test(`${message} ${responseText}`))
|
|
339
|
+
return "auth";
|
|
340
|
+
if (/ETIMEDOUT|timed?\s?out/i.test(message))
|
|
341
|
+
return "timeout";
|
|
342
|
+
return "unknown";
|
|
343
|
+
}
|
|
344
|
+
/** Map a classified failure to the operator-actionable message. */
|
|
345
|
+
function imapFailureMessage(kind, config, err) {
|
|
346
|
+
switch (kind) {
|
|
347
|
+
case "auth":
|
|
348
|
+
return `IMAP authentication failed for ${config.email}. Check the password — most providers require an app-specific password when 2FA is enabled.`;
|
|
349
|
+
case "refused":
|
|
350
|
+
return `IMAP connection refused at ${config.imapHost}:${config.imapPort}. Check that IMAP is enabled in your email provider settings.`;
|
|
351
|
+
case "not-found":
|
|
352
|
+
return `IMAP host ${config.imapHost} not found. Check the hostname.`;
|
|
353
|
+
case "tls":
|
|
354
|
+
return `IMAP TLS error connecting to ${config.imapHost}:${config.imapPort}. Try port 993 (TLS) or 143 (STARTTLS).`;
|
|
355
|
+
case "timeout":
|
|
356
|
+
return `IMAP connection timed out to ${config.imapHost}:${config.imapPort}. Check the host and port.`;
|
|
357
|
+
default:
|
|
358
|
+
return `IMAP connection failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
302
361
|
/**
|
|
303
362
|
* Test IMAP connectivity and authentication.
|
|
304
363
|
* Returns null on success, error message on failure.
|
|
364
|
+
*
|
|
365
|
+
* Emits one structured attempt line per probe (host/port/security/outcome/reason,
|
|
366
|
+
* no secret) so a failing setup is visible server-side without opening the session
|
|
367
|
+
* transcript.
|
|
305
368
|
*/
|
|
306
369
|
export async function testConnection(config, password, token = newProbeToken()) {
|
|
307
370
|
const client = createClient(config, password);
|
|
308
371
|
trackProbe("imap", client, token);
|
|
372
|
+
const attempt = (outcome, reason) => {
|
|
373
|
+
process.stderr.write(`[email] imap-attempt host=${config.imapHost} port=${config.imapPort} security=${config.imapSecurity} outcome=${outcome} reason=${reason}\n`);
|
|
374
|
+
};
|
|
309
375
|
try {
|
|
310
376
|
await client.connect();
|
|
377
|
+
// Socket is live once connect resolves — contain a late socket 'error' that
|
|
378
|
+
// could fire during logout/teardown.
|
|
379
|
+
containProbeSocket(client, "imap", token);
|
|
311
380
|
await safeLogout(client);
|
|
381
|
+
attempt("ok", "none");
|
|
312
382
|
return null;
|
|
313
383
|
}
|
|
314
384
|
catch (err) {
|
|
315
|
-
//
|
|
316
|
-
//
|
|
385
|
+
// On the auth-reject path ImapFlow leaves the socket live; contain it before
|
|
386
|
+
// close() removes ImapFlow's own forwarder, so a late socket 'error' cannot
|
|
387
|
+
// escape as an uncaughtException once the census has returned to baseline.
|
|
388
|
+
containProbeSocket(client, "imap", token);
|
|
317
389
|
try {
|
|
318
390
|
client.close();
|
|
319
391
|
}
|
|
320
392
|
catch { /* contained */ }
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
if (msg.includes("ECONNREFUSED")) {
|
|
326
|
-
return `IMAP connection refused at ${config.imapHost}:${config.imapPort}. Check that IMAP is enabled in your email provider settings.`;
|
|
327
|
-
}
|
|
328
|
-
if (msg.includes("ENOTFOUND")) {
|
|
329
|
-
return `IMAP host ${config.imapHost} not found. Check the hostname.`;
|
|
330
|
-
}
|
|
331
|
-
if (msg.includes("certificate") || msg.includes("TLS") || msg.includes("SSL")) {
|
|
332
|
-
return `IMAP TLS error connecting to ${config.imapHost}:${config.imapPort}. Try port 993 (TLS) or 143 (STARTTLS).`;
|
|
333
|
-
}
|
|
334
|
-
if (msg.includes("ETIMEDOUT") || msg.includes("timeout")) {
|
|
335
|
-
return `IMAP connection timed out to ${config.imapHost}:${config.imapPort}. Check the host and port.`;
|
|
336
|
-
}
|
|
337
|
-
return `IMAP connection failed: ${msg}`;
|
|
393
|
+
const kind = classifyImapError(err);
|
|
394
|
+
attempt("fail", kind);
|
|
395
|
+
return imapFailureMessage(kind, config, err);
|
|
338
396
|
}
|
|
339
397
|
finally {
|
|
340
398
|
releaseProbe("imap", token);
|
|
@@ -506,15 +564,15 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
506
564
|
let envelopeCount = 0;
|
|
507
565
|
let recipientKept = 0;
|
|
508
566
|
let recipientHidden = 0;
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
|
|
567
|
+
// Pass 1: envelopes + structure only (no source) across the whole
|
|
568
|
+
// over-fetched window, so recipient kept/hidden accounting covers every
|
|
569
|
+
// UID while costing only envelope bytes. structureType is captured per
|
|
570
|
+
// displayed message (aligned by index with `messages`) for the diag.
|
|
571
|
+
const displayStructureTypes = [];
|
|
513
572
|
for await (const msg of client.fetch(uidList, {
|
|
514
573
|
envelope: true,
|
|
515
574
|
uid: true,
|
|
516
575
|
bodyStructure: true,
|
|
517
|
-
source: true,
|
|
518
576
|
}, { uid: true })) {
|
|
519
577
|
envelopeCount++;
|
|
520
578
|
diag("fetchRecent", `envelope #${envelopeCount}`, `uid=${msg.uid} from=${msg.envelope?.from?.[0]?.address ?? "?"} at ${Date.now() - t2}ms`);
|
|
@@ -535,14 +593,10 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
535
593
|
continue;
|
|
536
594
|
// Collect up to `limit` messages for display, but keep iterating past
|
|
537
595
|
// that point so the recipient kept/hidden accounting covers the whole
|
|
538
|
-
// fetched window.
|
|
539
|
-
// (see bodySources) and only runs for the displayed messages.
|
|
596
|
+
// fetched window.
|
|
540
597
|
if (messages.length < limit) {
|
|
541
598
|
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
542
|
-
|
|
543
|
-
source: msg.source ?? Buffer.alloc(0),
|
|
544
|
-
structureType: msg.bodyStructure?.type ?? "?",
|
|
545
|
-
});
|
|
599
|
+
displayStructureTypes.push(msg.bodyStructure?.type ?? "?");
|
|
546
600
|
messages.push({
|
|
547
601
|
uid: msg.uid,
|
|
548
602
|
messageId,
|
|
@@ -555,11 +609,20 @@ export async function fetchRecent(config, password, options = {}) {
|
|
|
555
609
|
});
|
|
556
610
|
}
|
|
557
611
|
}
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
612
|
+
// Pass 2: download full source ONLY for the messages we display, keyed by
|
|
613
|
+
// UID, then decode previews now that the stream is closed. A heavily
|
|
614
|
+
// filtered over-fetch no longer pulls source for the UIDs it dropped.
|
|
615
|
+
if (messages.length > 0) {
|
|
616
|
+
const displayUids = messages.map((m) => m.uid);
|
|
617
|
+
const sourceByUid = new Map();
|
|
618
|
+
for await (const msg of client.fetch(displayUids.map(String).join(","), { uid: true, source: true }, { uid: true })) {
|
|
619
|
+
sourceByUid.set(msg.uid, msg.source ?? Buffer.alloc(0));
|
|
620
|
+
}
|
|
621
|
+
for (let i = 0; i < messages.length; i++) {
|
|
622
|
+
const { body, kind } = await extractBody(sourceByUid.get(messages[i].uid) ?? Buffer.alloc(0));
|
|
623
|
+
messages[i].bodyPreview = previewBody(body);
|
|
624
|
+
diag("fetchRecent", `body #${i + 1}`, `uid=${messages[i].uid} preview=${messages[i].bodyPreview.length}chars type=${displayStructureTypes[i]} bodySource=${kind}`);
|
|
625
|
+
}
|
|
563
626
|
}
|
|
564
627
|
diag("fetchRecent", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
565
628
|
if (clientSideRecipients) {
|
|
@@ -635,14 +698,14 @@ export async function searchMessages(config, password, query) {
|
|
|
635
698
|
let envelopeCount = 0;
|
|
636
699
|
let recipientKept = 0;
|
|
637
700
|
let recipientHidden = 0;
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
701
|
+
// Pass 1: envelopes + structure only (no source) across the whole
|
|
702
|
+
// over-fetched window (see fetchRecent). structureType is captured per
|
|
703
|
+
// displayed message, aligned by index with `messages`.
|
|
704
|
+
const displayStructureTypes = [];
|
|
641
705
|
for await (const msg of client.fetch(recentUids.map(String).join(","), {
|
|
642
706
|
envelope: true,
|
|
643
707
|
uid: true,
|
|
644
708
|
bodyStructure: true,
|
|
645
|
-
source: true,
|
|
646
709
|
}, { uid: true })) {
|
|
647
710
|
envelopeCount++;
|
|
648
711
|
// Client-side recipient filter (replaces IMAP SEARCH TO), with kept-vs-hidden accounting
|
|
@@ -660,13 +723,10 @@ export async function searchMessages(config, password, query) {
|
|
|
660
723
|
const inReplyTo = msg.envelope?.inReplyTo ?? null;
|
|
661
724
|
// Collect up to `limit` messages for display, but keep iterating past
|
|
662
725
|
// that point so the recipient kept/hidden accounting covers the whole
|
|
663
|
-
// fetched window.
|
|
726
|
+
// fetched window.
|
|
664
727
|
if (messages.length < limit) {
|
|
665
728
|
const attachments = walkAttachmentMetadata(msg.bodyStructure);
|
|
666
|
-
|
|
667
|
-
source: msg.source ?? Buffer.alloc(0),
|
|
668
|
-
structureType: msg.bodyStructure?.type ?? "?",
|
|
669
|
-
});
|
|
729
|
+
displayStructureTypes.push(msg.bodyStructure?.type ?? "?");
|
|
670
730
|
messages.push({
|
|
671
731
|
uid: msg.uid,
|
|
672
732
|
messageId,
|
|
@@ -679,11 +739,19 @@ export async function searchMessages(config, password, query) {
|
|
|
679
739
|
});
|
|
680
740
|
}
|
|
681
741
|
}
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
742
|
+
// Pass 2: download full source ONLY for the displayed messages, keyed by
|
|
743
|
+
// UID, then decode previews (see fetchRecent).
|
|
744
|
+
if (messages.length > 0) {
|
|
745
|
+
const displayUids = messages.map((m) => m.uid);
|
|
746
|
+
const sourceByUid = new Map();
|
|
747
|
+
for await (const msg of client.fetch(displayUids.map(String).join(","), { uid: true, source: true }, { uid: true })) {
|
|
748
|
+
sourceByUid.set(msg.uid, msg.source ?? Buffer.alloc(0));
|
|
749
|
+
}
|
|
750
|
+
for (let i = 0; i < messages.length; i++) {
|
|
751
|
+
const { body, kind } = await extractBody(sourceByUid.get(messages[i].uid) ?? Buffer.alloc(0));
|
|
752
|
+
messages[i].bodyPreview = previewBody(body);
|
|
753
|
+
diag("searchMessages", `body #${i + 1}`, `uid=${messages[i].uid} preview=${messages[i].bodyPreview.length}chars type=${displayStructureTypes[i]} bodySource=${kind}`);
|
|
754
|
+
}
|
|
687
755
|
}
|
|
688
756
|
diag("searchMessages", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
|
|
689
757
|
if (clientSideRecipients) {
|
|
@@ -823,67 +891,81 @@ export async function fetchSinceUid(config, password, sinceUid, recipients) {
|
|
|
823
891
|
}
|
|
824
892
|
}
|
|
825
893
|
/**
|
|
826
|
-
*
|
|
827
|
-
* the \Sent special-use folder
|
|
894
|
+
* Locate a message by Message-ID on an ALREADY-CONNECTED client. Searches
|
|
895
|
+
* INBOX first, then the \Sent special-use folder, returning the first hit
|
|
896
|
+
* whose envelope carries a From address. The caller owns connect/logout — this
|
|
897
|
+
* helper neither opens nor closes the connection, so one login can both resolve
|
|
898
|
+
* the message and fetch its source (see fetchFullBody's message_id path).
|
|
828
899
|
*
|
|
829
|
-
* Uses `header: { "message-id": ... }` IMAP SEARCH — sufficient on every
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
900
|
+
* Uses `header: { "message-id": ... }` IMAP SEARCH — sufficient on every server
|
|
901
|
+
* that honours RFC 3501 header search. The Message-ID is sent with angle
|
|
902
|
+
* brackets restored because some servers (notably Gmail) only match the raw
|
|
903
|
+
* header value, not the bare identifier.
|
|
833
904
|
*/
|
|
834
|
-
|
|
905
|
+
async function locateByMessageId(client, messageId) {
|
|
835
906
|
const cleanId = messageId.replace(/^<|>$/g, "");
|
|
836
907
|
const needle = `<${cleanId}>`;
|
|
908
|
+
const folders = ["INBOX"];
|
|
909
|
+
try {
|
|
910
|
+
const sentPath = await resolveFolderName(client, "sent");
|
|
911
|
+
folders.push(sentPath);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
// No \Sent folder advertised — only search INBOX.
|
|
915
|
+
}
|
|
916
|
+
for (const folderPath of folders) {
|
|
917
|
+
const lock = await client.getMailboxLock(folderPath);
|
|
918
|
+
try {
|
|
919
|
+
const uids = await client.search({ header: { "message-id": needle } }, { uid: true });
|
|
920
|
+
if (!uids || uids.length === 0)
|
|
921
|
+
continue;
|
|
922
|
+
const uid = uids[0];
|
|
923
|
+
for await (const msg of client.fetch(String(uid), { envelope: true, uid: true }, { uid: true })) {
|
|
924
|
+
const env = msg.envelope;
|
|
925
|
+
if (!env?.from?.[0]?.address)
|
|
926
|
+
continue;
|
|
927
|
+
return { uid: msg.uid, mailbox: folderPath, envelope: env };
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
finally {
|
|
931
|
+
lock.release();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Resolve an envelope by Message-ID via IMAP. Searches INBOX first, then
|
|
938
|
+
* the \Sent special-use folder. Returns null when no folder yields a hit.
|
|
939
|
+
*/
|
|
940
|
+
export async function fetchEnvelopeByMessageId(config, password, messageId) {
|
|
837
941
|
const client = createClient(config, password);
|
|
838
942
|
const t0 = Date.now();
|
|
839
|
-
diag("fetchEnvelopeByMessageId", "start", `messageId=${
|
|
943
|
+
diag("fetchEnvelopeByMessageId", "start", `messageId=${messageId.replace(/^<|>$/g, "")}`);
|
|
840
944
|
try {
|
|
841
945
|
await client.connect();
|
|
842
946
|
diag("fetchEnvelopeByMessageId", "connected", `${Date.now() - t0}ms`);
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
// No \Sent folder advertised — only search INBOX.
|
|
850
|
-
}
|
|
851
|
-
for (const folderPath of folders) {
|
|
852
|
-
const lock = await client.getMailboxLock(folderPath);
|
|
853
|
-
try {
|
|
854
|
-
const uids = await client.search({ header: { "message-id": needle } }, { uid: true });
|
|
855
|
-
if (!uids || uids.length === 0)
|
|
856
|
-
continue;
|
|
857
|
-
const uid = uids[0];
|
|
858
|
-
for await (const msg of client.fetch(String(uid), { envelope: true, uid: true }, { uid: true })) {
|
|
859
|
-
const env = msg.envelope ?? {};
|
|
860
|
-
const fromAddr = env.from?.[0]?.address ?? "";
|
|
861
|
-
if (!fromAddr)
|
|
862
|
-
continue;
|
|
863
|
-
diag("fetchEnvelopeByMessageId", "match", `folder=${folderPath} uid=${msg.uid} in ${Date.now() - t0}ms`);
|
|
864
|
-
return {
|
|
865
|
-
uid: msg.uid,
|
|
866
|
-
mailbox: folderPath,
|
|
867
|
-
messageId: env.messageId ?? messageId,
|
|
868
|
-
fromAddress: fromAddr,
|
|
869
|
-
fromName: env.from?.[0]?.name ?? null,
|
|
870
|
-
toAddresses: (env.to ?? [])
|
|
871
|
-
.map((a) => a.address)
|
|
872
|
-
.filter((a) => !!a),
|
|
873
|
-
ccAddresses: (env.cc ?? [])
|
|
874
|
-
.map((a) => a.address)
|
|
875
|
-
.filter((a) => !!a),
|
|
876
|
-
subject: env.subject ?? "",
|
|
877
|
-
date: env.date?.toISOString() ?? "",
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
finally {
|
|
882
|
-
lock.release();
|
|
883
|
-
}
|
|
947
|
+
const loc = await locateByMessageId(client, messageId);
|
|
948
|
+
if (!loc) {
|
|
949
|
+
diag("fetchEnvelopeByMessageId", "no-match", `in ${Date.now() - t0}ms`);
|
|
950
|
+
return null;
|
|
884
951
|
}
|
|
885
|
-
|
|
886
|
-
|
|
952
|
+
const env = loc.envelope;
|
|
953
|
+
diag("fetchEnvelopeByMessageId", "match", `folder=${loc.mailbox} uid=${loc.uid} in ${Date.now() - t0}ms`);
|
|
954
|
+
return {
|
|
955
|
+
uid: loc.uid,
|
|
956
|
+
mailbox: loc.mailbox,
|
|
957
|
+
messageId: env.messageId ?? messageId,
|
|
958
|
+
fromAddress: env.from?.[0]?.address ?? "",
|
|
959
|
+
fromName: env.from?.[0]?.name ?? null,
|
|
960
|
+
toAddresses: (env.to ?? [])
|
|
961
|
+
.map((a) => a.address)
|
|
962
|
+
.filter((a) => !!a),
|
|
963
|
+
ccAddresses: (env.cc ?? [])
|
|
964
|
+
.map((a) => a.address)
|
|
965
|
+
.filter((a) => !!a),
|
|
966
|
+
subject: env.subject ?? "",
|
|
967
|
+
date: env.date?.toISOString() ?? "",
|
|
968
|
+
};
|
|
887
969
|
}
|
|
888
970
|
catch (err) {
|
|
889
971
|
diag("fetchEnvelopeByMessageId", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
|
|
@@ -900,18 +982,6 @@ export async function fetchEnvelopeByMessageId(config, password, messageId) {
|
|
|
900
982
|
* MIME-shape blind spot. Returns null when the message cannot be located.
|
|
901
983
|
*/
|
|
902
984
|
export async function fetchFullBody(config, password, target) {
|
|
903
|
-
let mailbox = "";
|
|
904
|
-
let uid;
|
|
905
|
-
if ("messageId" in target) {
|
|
906
|
-
const env = await fetchEnvelopeByMessageId(config, password, target.messageId);
|
|
907
|
-
if (!env)
|
|
908
|
-
return null;
|
|
909
|
-
mailbox = env.mailbox;
|
|
910
|
-
uid = env.uid;
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
uid = target.uid;
|
|
914
|
-
}
|
|
915
985
|
const client = createClient(config, password);
|
|
916
986
|
const t0 = Date.now();
|
|
917
987
|
const label = "messageId" in target
|
|
@@ -920,8 +990,22 @@ export async function fetchFullBody(config, password, target) {
|
|
|
920
990
|
diag("fetchFullBody", "start", label);
|
|
921
991
|
try {
|
|
922
992
|
await client.connect();
|
|
923
|
-
|
|
993
|
+
// Resolve the message location on THIS connection — a message_id read no
|
|
994
|
+
// longer opens a second login just to learn its uid/mailbox.
|
|
995
|
+
let mailbox;
|
|
996
|
+
let uid;
|
|
997
|
+
if ("messageId" in target) {
|
|
998
|
+
const loc = await locateByMessageId(client, target.messageId);
|
|
999
|
+
if (!loc) {
|
|
1000
|
+
diag("fetchFullBody", "not-found", `messageId=${target.messageId} in ${Date.now() - t0}ms`);
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
mailbox = loc.mailbox;
|
|
1004
|
+
uid = loc.uid;
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
924
1007
|
mailbox = await resolveFolderName(client, target.folder ?? "inbox");
|
|
1008
|
+
uid = target.uid;
|
|
925
1009
|
}
|
|
926
1010
|
const lock = await client.getMailboxLock(mailbox);
|
|
927
1011
|
try {
|
|
@@ -965,6 +1049,7 @@ export async function getInboxCount(config, password, token = newProbeToken()) {
|
|
|
965
1049
|
trackProbe("imap", client, token);
|
|
966
1050
|
try {
|
|
967
1051
|
await client.connect();
|
|
1052
|
+
containProbeSocket(client, "imap", token);
|
|
968
1053
|
const mailbox = await client.mailboxOpen("INBOX");
|
|
969
1054
|
return mailbox.exists;
|
|
970
1055
|
}
|