@agenticmail/api 0.9.4 → 0.9.6
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/index.js +59 -14
- package/package.json +2 -2
- package/public/js/app.js +33 -12
- package/public/js/list-view.js +9 -1
package/dist/index.js
CHANGED
|
@@ -2714,30 +2714,75 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
|
|
|
2714
2714
|
const folder = req.query.folder || "INBOX";
|
|
2715
2715
|
const password = getAgentPassword(agent);
|
|
2716
2716
|
const receiver = await getReceiver(agent.stalwartPrincipal, password, config);
|
|
2717
|
-
const
|
|
2718
|
-
const
|
|
2719
|
-
const
|
|
2720
|
-
const
|
|
2721
|
-
const
|
|
2722
|
-
|
|
2723
|
-
|
|
2717
|
+
const PREVIEW_MAX_BYTES = 8192;
|
|
2718
|
+
const client = receiver.getImapClient();
|
|
2719
|
+
const lock = await client.getMailboxLock(folder);
|
|
2720
|
+
const envelopes = [];
|
|
2721
|
+
const rawMap = /* @__PURE__ */ new Map();
|
|
2722
|
+
let total = 0;
|
|
2723
|
+
try {
|
|
2724
|
+
const searchResult = await client.search({ all: true }, { uid: true });
|
|
2725
|
+
const allUids = Array.isArray(searchResult) ? searchResult : [];
|
|
2726
|
+
total = allUids.length;
|
|
2727
|
+
const sorted = allUids.slice().sort((a, b) => b - a);
|
|
2728
|
+
const pageUids = sorted.slice(offset, offset + limit);
|
|
2729
|
+
if (pageUids.length > 0) {
|
|
2730
|
+
for await (const msg of client.fetch(pageUids.join(","), {
|
|
2731
|
+
uid: true,
|
|
2732
|
+
envelope: true,
|
|
2733
|
+
flags: true,
|
|
2734
|
+
size: true
|
|
2735
|
+
})) {
|
|
2736
|
+
const env = msg.envelope;
|
|
2737
|
+
if (!env) continue;
|
|
2738
|
+
envelopes.push({
|
|
2739
|
+
uid: msg.uid,
|
|
2740
|
+
subject: env.subject ?? "",
|
|
2741
|
+
from: (env.from ?? []).map((a) => ({ name: a.name, address: a.address ?? "" })),
|
|
2742
|
+
to: (env.to ?? []).map((a) => ({ name: a.name, address: a.address ?? "" })),
|
|
2743
|
+
date: env.date ?? /* @__PURE__ */ new Date(),
|
|
2744
|
+
flags: msg.flags ? [...msg.flags] : [],
|
|
2745
|
+
size: msg.size ?? 0
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
envelopes.sort((a, b) => b.uid - a.uid);
|
|
2749
|
+
for await (const msg of client.fetch(pageUids.join(","), {
|
|
2750
|
+
uid: true,
|
|
2751
|
+
source: { start: 0, maxLength: PREVIEW_MAX_BYTES }
|
|
2752
|
+
})) {
|
|
2753
|
+
if (msg.source) {
|
|
2754
|
+
rawMap.set(
|
|
2755
|
+
msg.uid,
|
|
2756
|
+
Buffer.isBuffer(msg.source) ? msg.source : Buffer.from(msg.source)
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
} finally {
|
|
2762
|
+
lock.release();
|
|
2763
|
+
}
|
|
2764
|
+
const messages = await Promise.all(envelopes.map(async (env) => {
|
|
2724
2765
|
const raw = rawMap.get(env.uid);
|
|
2766
|
+
let preview = "";
|
|
2725
2767
|
if (raw) {
|
|
2726
|
-
|
|
2727
|
-
|
|
2768
|
+
try {
|
|
2769
|
+
const parsed = await parseEmail2(raw);
|
|
2770
|
+
preview = (parsed.text || "").slice(0, previewLen);
|
|
2771
|
+
} catch {
|
|
2772
|
+
}
|
|
2728
2773
|
}
|
|
2729
|
-
|
|
2774
|
+
return {
|
|
2730
2775
|
uid: env.uid,
|
|
2731
2776
|
subject: env.subject,
|
|
2732
2777
|
from: env.from,
|
|
2733
2778
|
to: env.to,
|
|
2734
2779
|
date: env.date,
|
|
2735
|
-
flags:
|
|
2780
|
+
flags: env.flags,
|
|
2736
2781
|
size: env.size,
|
|
2737
2782
|
preview
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
res.json({ messages, count: messages.length, total
|
|
2783
|
+
};
|
|
2784
|
+
}));
|
|
2785
|
+
res.json({ messages, count: messages.length, total });
|
|
2741
2786
|
} catch (err) {
|
|
2742
2787
|
next(err);
|
|
2743
2788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/api",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"uuid": "^11.1.0"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@agenticmail/claudecode": "^0.2.
|
|
39
|
+
"@agenticmail/claudecode": "^0.2.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/cors": "^2.8.17",
|
package/public/js/app.js
CHANGED
|
@@ -95,6 +95,19 @@ async function bootstrap() {
|
|
|
95
95
|
const initial = (lastId && state.agents.find(a => a.id === lastId))
|
|
96
96
|
?? state.agents.find(isBridgeAgent)
|
|
97
97
|
?? state.agents[0];
|
|
98
|
+
// Seed the URL hash BEFORE selectAgent so selectAgent's loadList
|
|
99
|
+
// call lands on the right folder. We use history.replaceState
|
|
100
|
+
// (NOT `location.hash = ...`) so this does NOT fire a hashchange
|
|
101
|
+
// event — that would trigger a second route() → loadList() in
|
|
102
|
+
// parallel with selectAgent's, doubling the work on every
|
|
103
|
+
// bootstrap. Read the hash first so a deep-link refresh
|
|
104
|
+
// (e.g. /#/folder/sent) still wins.
|
|
105
|
+
const folderMatch = location.hash.match(/^#\/folder\/([a-z]+)$/);
|
|
106
|
+
if (folderMatch) {
|
|
107
|
+
state.selectedFolder = folderMatch[1];
|
|
108
|
+
} else if (!location.hash) {
|
|
109
|
+
history.replaceState(null, '', `${location.pathname}${location.search}#/folder/inbox`);
|
|
110
|
+
}
|
|
98
111
|
if (initial) await selectAgent(initial);
|
|
99
112
|
renderProfile();
|
|
100
113
|
populateComposeFrom();
|
|
@@ -105,10 +118,14 @@ async function bootstrap() {
|
|
|
105
118
|
// rendering. Idempotent — safe to call after bootstrap reruns.
|
|
106
119
|
subscribeToActivity();
|
|
107
120
|
maybeRequestNotificationPermission();
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
// If the URL points at a message (not a folder), open it now —
|
|
122
|
+
// the folder list selectAgent already loaded stays in the
|
|
123
|
+
// background. Folder hashes need no extra work; selectAgent's
|
|
124
|
+
// loadList already handled them above.
|
|
125
|
+
const hash = location.hash;
|
|
126
|
+
const msgMatch = hash.match(/^#\/m\/(\d+)$/);
|
|
127
|
+
const draftMatch = hash.match(/^#\/d\/([a-zA-Z0-9-]+)$/);
|
|
128
|
+
if (msgMatch || draftMatch) route();
|
|
112
129
|
} catch (err) {
|
|
113
130
|
toast(`Failed to load agents: ${err.message}`, true);
|
|
114
131
|
}
|
|
@@ -176,14 +193,18 @@ function route() {
|
|
|
176
193
|
}
|
|
177
194
|
const folderMatch = hash.match(/^#\/folder\/([a-z]+)$/);
|
|
178
195
|
const folder = folderMatch ? folderMatch[1] : 'inbox';
|
|
179
|
-
if
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
// Only do work if the folder actually changed. Re-firing loadList
|
|
197
|
+
// for the SAME folder on every hashchange (e.g. closing a message
|
|
198
|
+
// detail back to the list) makes the UI feel sluggish — the list
|
|
199
|
+
// is already rendered, and a second digest fetch just churns
|
|
200
|
+
// through 50 messages on the IMAP server for no visible change.
|
|
201
|
+
if (state.selectedFolder === folder) return;
|
|
202
|
+
state.selectedFolder = folder;
|
|
203
|
+
// Reset pagination on every folder switch — a fresh folder
|
|
204
|
+
// starts at page 1. Preserved across silent SSE refreshes so
|
|
205
|
+
// a new arrival doesn't yank the user back from page 3.
|
|
206
|
+
state.pagination = { offset: 0, limit: 50, total: 0 };
|
|
207
|
+
renderSidebar(onFolderSelect);
|
|
187
208
|
if (state.selectedAgent) loadList(state.selectedAgent, folder);
|
|
188
209
|
}
|
|
189
210
|
window.addEventListener('hashchange', route);
|
package/public/js/list-view.js
CHANGED
|
@@ -359,7 +359,15 @@ export function renderList() {
|
|
|
359
359
|
const prevBtn = document.getElementById('pager-prev');
|
|
360
360
|
const nextBtn = document.getElementById('pager-next');
|
|
361
361
|
if (prevBtn) prevBtn.disabled = offset <= 0;
|
|
362
|
-
|
|
362
|
+
// Drive Next purely from the server-reported total. The previous
|
|
363
|
+
// `state.messages.length < limit` clause was meant as a "we hit the
|
|
364
|
+
// end" heuristic for the no-total fallback case, but it backfired
|
|
365
|
+
// on every folder that legitimately had fewer-than-`limit` items on
|
|
366
|
+
// a page (e.g. trailing partial page after deletions, or a folder
|
|
367
|
+
// whose IMAP STATUS returned a stale low count) — Next stayed
|
|
368
|
+
// permanently disabled. The new digest endpoint always returns an
|
|
369
|
+
// authoritative SEARCH-derived total, so a single check is enough.
|
|
370
|
+
if (nextBtn) nextBtn.disabled = pageEnd >= total;
|
|
363
371
|
|
|
364
372
|
if (filtered.length === 0) {
|
|
365
373
|
root.innerHTML = q
|