dsh-email 0.17.3 → 0.18.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/CHANGELOG.md +7 -0
- package/README.en.md +6 -4
- package/README.md +6 -4
- package/docs/AGENTMAIL.en.md +37 -0
- package/docs/AGENTMAIL.md +41 -0
- package/docs/USAGE.en.md +2 -0
- package/docs/USAGE.md +2 -0
- package/docs/VALIDATION.md +2 -0
- package/docs/validation/0.18.0.md +23 -0
- package/lib/agentmail.d.ts +33 -0
- package/lib/agentmail.js +407 -0
- package/lib/client.js +129 -31
- package/lib/config.d.ts +4 -0
- package/lib/config.js +12 -2
- package/lib/draft-store.d.ts +5 -0
- package/lib/draft-store.js +4 -0
- package/lib/draft-web.js +15 -6
- package/lib/host-config.js +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +12 -2
- package/lib/mail-client.d.ts +5 -1
- package/lib/mail-client.js +20 -5
- package/lib/mail-service.d.ts +50 -0
- package/lib/mail-service.js +128 -0
- package/lib/mailbox-web.d.ts +11 -0
- package/lib/mailbox-web.js +219 -0
- package/lib/public-download.d.ts +3 -0
- package/lib/public-download.js +77 -0
- package/lib/runtime.d.ts +2 -2
- package/lib/runtime.js +7 -3
- package/lib/settings.js +1 -1
- package/lib/tool-contract.d.ts +37 -8
- package/lib/tool-contract.js +30 -18
- package/lib/tools.js +10 -9
- package/lib/types.d.ts +18 -10
- package/lib/web.d.ts +9 -1
- package/lib/web.js +20 -2
- package/package.json +4 -3
package/lib/tool-contract.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare const ACCOUNT_HINT = "\u8D26\u53F7\u540D\uFF08\u914D\u7F6E\u4E86
|
|
|
10
10
|
export declare function parseEmailDay(input: string, label: string, endInclusive?: boolean): Date;
|
|
11
11
|
export declare function executionSignal(exec: unknown): AbortSignal | undefined;
|
|
12
12
|
interface ParameterProperty {
|
|
13
|
-
type?: string;
|
|
13
|
+
type?: string | string[];
|
|
14
14
|
required?: boolean;
|
|
15
15
|
description?: string;
|
|
16
16
|
items?: {
|
|
@@ -34,7 +34,9 @@ export declare const addrArray: {
|
|
|
34
34
|
};
|
|
35
35
|
export declare const messageShape: {
|
|
36
36
|
uid: {
|
|
37
|
-
|
|
37
|
+
oneOf: {
|
|
38
|
+
type: string;
|
|
39
|
+
}[];
|
|
38
40
|
};
|
|
39
41
|
date: {
|
|
40
42
|
type: string;
|
|
@@ -71,6 +73,18 @@ export declare const listSchema: {
|
|
|
71
73
|
count: {
|
|
72
74
|
type: string;
|
|
73
75
|
};
|
|
76
|
+
countExact: {
|
|
77
|
+
type: string;
|
|
78
|
+
};
|
|
79
|
+
hasMore: {
|
|
80
|
+
type: string;
|
|
81
|
+
};
|
|
82
|
+
order: {
|
|
83
|
+
type: string;
|
|
84
|
+
};
|
|
85
|
+
searchSource: {
|
|
86
|
+
type: string;
|
|
87
|
+
};
|
|
74
88
|
folder: {
|
|
75
89
|
type: string;
|
|
76
90
|
};
|
|
@@ -80,7 +94,9 @@ export declare const listSchema: {
|
|
|
80
94
|
type: string;
|
|
81
95
|
properties: {
|
|
82
96
|
uid: {
|
|
83
|
-
|
|
97
|
+
oneOf: {
|
|
98
|
+
type: string;
|
|
99
|
+
}[];
|
|
84
100
|
};
|
|
85
101
|
date: {
|
|
86
102
|
type: string;
|
|
@@ -121,7 +137,9 @@ export declare const readSchema: {
|
|
|
121
137
|
type: string;
|
|
122
138
|
};
|
|
123
139
|
uid: {
|
|
124
|
-
|
|
140
|
+
oneOf: {
|
|
141
|
+
type: string;
|
|
142
|
+
}[];
|
|
125
143
|
};
|
|
126
144
|
folder: {
|
|
127
145
|
type: string;
|
|
@@ -233,7 +251,9 @@ export declare const attachmentSchema: {
|
|
|
233
251
|
type: string;
|
|
234
252
|
};
|
|
235
253
|
uid: {
|
|
236
|
-
|
|
254
|
+
oneOf: {
|
|
255
|
+
type: string;
|
|
256
|
+
}[];
|
|
237
257
|
};
|
|
238
258
|
filename: {
|
|
239
259
|
type: string;
|
|
@@ -257,7 +277,9 @@ export declare const markSchema: {
|
|
|
257
277
|
type: string;
|
|
258
278
|
};
|
|
259
279
|
uid: {
|
|
260
|
-
|
|
280
|
+
oneOf: {
|
|
281
|
+
type: string;
|
|
282
|
+
}[];
|
|
261
283
|
};
|
|
262
284
|
folder: {
|
|
263
285
|
type: string;
|
|
@@ -306,7 +328,9 @@ export declare const replySchema: {
|
|
|
306
328
|
type: string;
|
|
307
329
|
};
|
|
308
330
|
originalUid: {
|
|
309
|
-
|
|
331
|
+
oneOf: {
|
|
332
|
+
type: string;
|
|
333
|
+
}[];
|
|
310
334
|
};
|
|
311
335
|
messageId: {
|
|
312
336
|
type: string;
|
|
@@ -359,13 +383,18 @@ export declare const watchSchema: {
|
|
|
359
383
|
totalUnread: {
|
|
360
384
|
type: string;
|
|
361
385
|
};
|
|
386
|
+
totalUnreadExact: {
|
|
387
|
+
type: string;
|
|
388
|
+
};
|
|
362
389
|
messages: {
|
|
363
390
|
type: string;
|
|
364
391
|
items: {
|
|
365
392
|
type: string;
|
|
366
393
|
properties: {
|
|
367
394
|
uid: {
|
|
368
|
-
|
|
395
|
+
oneOf: {
|
|
396
|
+
type: string;
|
|
397
|
+
}[];
|
|
369
398
|
};
|
|
370
399
|
date: {
|
|
371
400
|
type: string;
|
package/lib/tool-contract.js
CHANGED
|
@@ -35,6 +35,8 @@ export function compileParameters(spec) {
|
|
|
35
35
|
const node = {};
|
|
36
36
|
if (typeof prop?.type === 'string')
|
|
37
37
|
node.type = prop.type;
|
|
38
|
+
else if (Array.isArray(prop?.type))
|
|
39
|
+
node.oneOf = prop.type.map(type => ({ type }));
|
|
38
40
|
if (typeof prop?.description === 'string')
|
|
39
41
|
node.description = prop.description;
|
|
40
42
|
if (prop?.type === 'array' && prop.items !== null && typeof prop.items === 'object') {
|
|
@@ -58,7 +60,7 @@ export function normalizeAttachmentPaths(value) {
|
|
|
58
60
|
export const strArray = { type: 'array', items: { type: 'string' } };
|
|
59
61
|
export const addrArray = { type: 'array', items: { type: 'object', additionalProperties: true } };
|
|
60
62
|
export const messageShape = {
|
|
61
|
-
uid: { type: 'integer' },
|
|
63
|
+
uid: { oneOf: [{ type: 'integer' }, { type: 'string' }] },
|
|
62
64
|
date: { type: 'string' },
|
|
63
65
|
from: addrArray,
|
|
64
66
|
subject: { type: 'string' },
|
|
@@ -72,6 +74,10 @@ export const listSchema = {
|
|
|
72
74
|
properties: {
|
|
73
75
|
account: { type: 'string' },
|
|
74
76
|
count: { type: 'integer' },
|
|
77
|
+
countExact: { type: 'boolean' },
|
|
78
|
+
hasMore: { type: 'boolean' },
|
|
79
|
+
order: { type: 'string' },
|
|
80
|
+
searchSource: { type: 'string' },
|
|
75
81
|
folder: { type: 'string' },
|
|
76
82
|
messages: { type: 'array', items: { type: 'object', properties: messageShape, additionalProperties: true } },
|
|
77
83
|
},
|
|
@@ -81,7 +87,7 @@ export const readSchema = {
|
|
|
81
87
|
type: 'object',
|
|
82
88
|
properties: {
|
|
83
89
|
account: { type: 'string' },
|
|
84
|
-
uid: { type: 'integer' },
|
|
90
|
+
uid: { oneOf: [{ type: 'integer' }, { type: 'string' }] },
|
|
85
91
|
folder: { type: 'string' },
|
|
86
92
|
date: { type: 'string' },
|
|
87
93
|
from: addrArray,
|
|
@@ -129,7 +135,7 @@ export const attachmentSchema = {
|
|
|
129
135
|
type: 'object',
|
|
130
136
|
properties: {
|
|
131
137
|
account: { type: 'string' },
|
|
132
|
-
uid: { type: 'integer' },
|
|
138
|
+
uid: { oneOf: [{ type: 'integer' }, { type: 'string' }] },
|
|
133
139
|
filename: { type: 'string' },
|
|
134
140
|
contentType: { type: 'string' },
|
|
135
141
|
size: { type: 'integer' },
|
|
@@ -141,7 +147,7 @@ export const markSchema = {
|
|
|
141
147
|
type: 'object',
|
|
142
148
|
properties: {
|
|
143
149
|
account: { type: 'string' },
|
|
144
|
-
uid: { type: 'integer' },
|
|
150
|
+
uid: { oneOf: [{ type: 'integer' }, { type: 'string' }] },
|
|
145
151
|
folder: { type: 'string' },
|
|
146
152
|
action: { type: 'string' },
|
|
147
153
|
seen: { type: 'boolean' },
|
|
@@ -181,6 +187,8 @@ export function describeMessage(message) {
|
|
|
181
187
|
return (message.subject || '(无主题)') + ' [' + parts.join(' | ') + ']';
|
|
182
188
|
}
|
|
183
189
|
export function renderList(value) {
|
|
190
|
+
if (value.countExact !== undefined)
|
|
191
|
+
return oneText('账号 ' + value.account + ',文件夹 ' + value.folder + ':本页 ' + value.messages.length + ' 封' + (value.hasMore ? ',还有更多' : '') + '(不把 API 页数当作邮箱总数)\n\n' + value.messages.map(describeMessage).join('\n'));
|
|
184
192
|
if (value.messages.length === 0) {
|
|
185
193
|
return oneText('账号 ' + value.account + ',文件夹 "' + value.folder + '" 共 ' + value.count + ' 封邮件,本次没有要列出的邮件。');
|
|
186
194
|
}
|
|
@@ -195,6 +203,8 @@ export function renderRead(value) {
|
|
|
195
203
|
return oneText('账号 ' + value.account + ',主题:' + (value.subject || '(无主题)') + '\n来自:' + from + '\n时间:' + (value.date || '(未知)') + attach + '\n\n' + value.text);
|
|
196
204
|
}
|
|
197
205
|
export function renderSearch(value) {
|
|
206
|
+
if (value.searchSource === 'api')
|
|
207
|
+
return oneText('AgentMail 搜索(按相关性,服务排除垃圾箱等特殊邮件):本页 ' + value.messages.length + ' 条' + (value.hasMore ? ',还有更多;可增加 offset 继续' : '') + '\n\n' + value.messages.map(describeMessage).join('\n'));
|
|
198
208
|
if (value.headerScan) {
|
|
199
209
|
const scan = value.headerScan;
|
|
200
210
|
const window = scan.scanOffset === 0 ? '最近 ' + scan.scanned : '跳过最新 ' + scan.scanOffset + ' 封后的 ' + scan.scanned;
|
|
@@ -258,14 +268,15 @@ export function renderReply(value) {
|
|
|
258
268
|
return oneText('账号 ' + value.account + ' 已' + REPLY_LABELS[value.mode] + ' uid=' + value.originalUid + ' 的邮件:收件人 ' + value.to.join(', ') + ',主题「' + value.subject + '」,messageId: ' + value.messageId + rejected);
|
|
259
269
|
}
|
|
260
270
|
export function renderWatch(value) {
|
|
271
|
+
const unread = (value.totalUnreadExact === false ? '本次扫描至少未读 ' : '当前未读 ') + value.totalUnread + ' 封';
|
|
261
272
|
if (value.reset === true) {
|
|
262
273
|
return oneText('账号 ' + value.account + ':文件夹 "' + value.folder + '" 的 UIDVALIDITY 已变化(服务器重新编号了邮件),已重新建立基线(当前未读 ' + value.totalUnread + ' 封)。这次不报告新邮件,之后照常。');
|
|
263
274
|
}
|
|
264
275
|
if (value.firstRun) {
|
|
265
|
-
return oneText('账号 ' + value.account + '
|
|
276
|
+
return oneText('账号 ' + value.account + ':已建立新邮件监视基线(' + unread + ')。之后调用 email_watch 只会报告新到的邮件。');
|
|
266
277
|
}
|
|
267
278
|
if (value.newCount === 0) {
|
|
268
|
-
return oneText('账号 ' + value.account + '
|
|
279
|
+
return oneText('账号 ' + value.account + ':没有新邮件(' + unread + ')。');
|
|
269
280
|
}
|
|
270
281
|
const lines = value.messages.map((m, i) => '#' + (i + 1) + ' ' + describeMessage(m));
|
|
271
282
|
return oneText('账号 ' + value.account + ' 有 ' + value.newCount + ' 封新邮件:\n\n' + lines.join('\n') + '\n\n用 email_read 配合 uid 阅读全文。');
|
|
@@ -285,7 +296,7 @@ export const replySchema = {
|
|
|
285
296
|
properties: {
|
|
286
297
|
account: { type: 'string' },
|
|
287
298
|
mode: { type: 'string' },
|
|
288
|
-
originalUid: { type: 'integer' },
|
|
299
|
+
originalUid: { oneOf: [{ type: 'integer' }, { type: 'string' }] },
|
|
289
300
|
messageId: { type: 'string' },
|
|
290
301
|
accepted: strArray,
|
|
291
302
|
rejected: strArray,
|
|
@@ -304,20 +315,21 @@ export const watchSchema = {
|
|
|
304
315
|
reset: { type: 'boolean' },
|
|
305
316
|
newCount: { type: 'integer' },
|
|
306
317
|
totalUnread: { type: 'integer' },
|
|
318
|
+
totalUnreadExact: { type: 'boolean' },
|
|
307
319
|
messages: { type: 'array', items: { type: 'object', properties: messageShape, additionalProperties: true } },
|
|
308
320
|
},
|
|
309
321
|
additionalProperties: true,
|
|
310
322
|
};
|
|
311
323
|
export const descriptions = {
|
|
312
|
-
"email_list": 'List recent emails in a mailbox folder (newest first). Returns uid, date, sender, subject and flags without message bodies; use email_read with a uid to fetch the full text. Optional since/until (dates like 2026-08-01) filter by received date.',
|
|
324
|
+
"email_list": 'IMAP uid is a positive integer; AgentMail uid is an opaque string message_id. AgentMail returns page counts: inspect hasMore/countExact instead of assuming mailbox totals. List recent emails in a mailbox folder (newest first). Returns uid, date, sender, subject and flags without message bodies; use email_read with a uid to fetch the full text. Optional since/until (dates like 2026-08-01) filter by received date.',
|
|
313
325
|
"email_read": 'Read one full email message by its uid (from email_list or email_search). Returns the plain-text body (HTML mail is converted; oversized bodies are truncated) plus attachment metadata; use email_attachment to download one.',
|
|
314
|
-
"email_mark": 'Change an existing message: mark it read/unread, star/unstar it, or move it to another folder. Use after email_list/email_search when the user wants to tidy the mailbox (archive, clear unread, flag important mail). Moving uses the server MOVE/COPY so the uid changes; the new uid is reported when the server provides it.',
|
|
315
|
-
"email_search": 'Search sender, recipients and subject; verify server candidates against envelopes and arrival dates. If server SEARCH is empty, fails, or has no confirmed matches, scan local headers in batches without downloading bodies/attachments (default newest 5000, max 20000). headerScanOffset/Limit select an older header window; use headerScan.nextScanOffset to continue. offset pages MATCHES within that window. If no headers match and headerScanOffset=0, optional body fallback scans only bodySearchLimit recent arrivals. Inspect searchSource/headerScan/bodyScan: partial or zero results never prove the whole mailbox has no match. Results use descending mailbox UID arrival order, not Date-header or INTERNALDATE sorting. since/until filter INTERNALDATE (Date header only if unavailable).',
|
|
316
|
-
"email_send": 'Send an email from a configured account, optionally with file attachments (absolute paths, or relative to the current session workspace; process cwd is the fallback without a session). Saved blocked-address/domain rules reject To/Cc recipients even when sendApproval is off. Plugin confirmation is required by default, unless sendApproval is disabled or every To/Cc recipient matches saved trust rules with skipApproval enabled. Host permissions still apply. Full Access refuses a send that still requires plugin confirmation; use email_draft for human review and explicit sending. Never invent recipients or content without the user\'s instruction.',
|
|
317
|
-
"email_reply": 'Reply to, reply-all to, or forward an existing message (mode: reply | reply-all | forward). Recipients come from the original message (your own address is excluded automatically), the subject gets a single Re:/Fwd: prefix, the original text is quoted underneath, and In-Reply-To/References headers keep mail clients threading correctly. mode=forward needs the to parameter. Plugin confirmation is required unless sendApproval is disabled; trusted-recipient skipping only applies to email_send. Blocked recipients still stop delivery before SMTP and host permissions remain in effect. Never invent recipients or content without the user\'s instruction.',
|
|
326
|
+
"email_mark": 'AgentMail supports flags and only INBOX-to-Archive label archiving; arbitrary IMAP moves are not supported there. Change an existing message: mark it read/unread, star/unstar it, or move it to another folder. Use after email_list/email_search when the user wants to tidy the mailbox (archive, clear unread, flag important mail). Moving uses the server MOVE/COPY so the uid changes; the new uid is reported when the server provides it.',
|
|
327
|
+
"email_search": 'AgentMail accounts use API relevance search (searchSource=api), exclude spam/trash/blocked/unauthenticated mail, and page with offset; headerScanOffset and IMAP fallbacks do not apply. For IMAP: Search sender, recipients and subject; verify server candidates against envelopes and arrival dates. If server SEARCH is empty, fails, or has no confirmed matches, scan local headers in batches without downloading bodies/attachments (default newest 5000, max 20000). headerScanOffset/Limit select an older header window; use headerScan.nextScanOffset to continue. offset pages MATCHES within that window. If no headers match and headerScanOffset=0, optional body fallback scans only bodySearchLimit recent arrivals. Inspect searchSource/headerScan/bodyScan: partial or zero results never prove the whole mailbox has no match. Results use descending mailbox UID arrival order, not Date-header or INTERNALDATE sorting. since/until filter INTERNALDATE (Date header only if unavailable).',
|
|
328
|
+
"email_send": 'AgentMail has a plugin limit of 10 attachments / 4 MiB total and uses its inbox API; a receipt means submission, not delivery. Send an email from a configured account, optionally with file attachments (absolute paths, or relative to the current session workspace; process cwd is the fallback without a session). Saved blocked-address/domain rules reject To/Cc recipients even when sendApproval is off. Plugin confirmation is required by default, unless sendApproval is disabled or every To/Cc recipient matches saved trust rules with skipApproval enabled. Host permissions still apply. Full Access refuses a send that still requires plugin confirmation; use email_draft for human review and explicit sending. Never invent recipients or content without the user\'s instruction.',
|
|
329
|
+
"email_reply": 'AgentMail reply/reply-all uses its thread-aware reply API; forwarding creates a new message. Reply to, reply-all to, or forward an existing message (mode: reply | reply-all | forward). Recipients come from the original message (your own address is excluded automatically), the subject gets a single Re:/Fwd: prefix, the original text is quoted underneath, and In-Reply-To/References headers keep mail clients threading correctly. mode=forward needs the to parameter. Plugin confirmation is required unless sendApproval is disabled; trusted-recipient skipping only applies to email_send. Blocked recipients still stop delivery before SMTP and host permissions remain in effect. Never invent recipients or content without the user\'s instruction.',
|
|
318
330
|
"email_folders": 'List the mailbox folders of an account (INBOX, Sent, Trash, custom folders, ...). Use the returned path values as the folder argument of the other email tools.',
|
|
319
|
-
"email_health": 'Self-check for dsh-email: summarizes configured accounts (provider / IMAP / SMTP hosts) without any network connection and never shows passwords. Run this first when troubleshooting.',
|
|
320
|
-
"email_attachment": 'Download one attachment of a message to a local file (size capped by maxAttachmentBytes). The index matches the attachments array of email_read. Returns the absolute path of the written file.',
|
|
331
|
+
"email_health": 'AgentMail accounts show fixed regional API endpoints instead of IMAP/SMTP servers. Self-check for dsh-email: summarizes configured accounts (provider / IMAP / SMTP hosts) without any network connection and never shows passwords. Run this first when troubleshooting.',
|
|
332
|
+
"email_attachment": 'AgentMail downloads are limited to 20 MiB or the smaller configured maximum. Download one attachment of a message to a local file (size capped by maxAttachmentBytes). The index matches the attachments array of email_read. Returns the absolute path of the written file.',
|
|
321
333
|
"email_watch": 'Check for NEW unread emails since the last check (cursor-based). The first call per account+folder sets the baseline and reports nothing as new; every later call returns only unseen unread messages. Call this periodically (e.g. via a scheduled task) to notify the user about new mail. Returns newCount, the new messages, and totalUnread.'
|
|
322
334
|
};
|
|
323
335
|
export const parameters = {
|
|
@@ -331,12 +343,12 @@ export const parameters = {
|
|
|
331
343
|
account: { type: 'string', description: ACCOUNT_HINT },
|
|
332
344
|
}),
|
|
333
345
|
"email_read": compileParameters({
|
|
334
|
-
uid: { type: 'integer', required: true, description: '
|
|
346
|
+
uid: { type: ['integer', 'string'], required: true, description: 'IMAP positive integer uid or unchanged AgentMail string message_id from email_list or email_search' },
|
|
335
347
|
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
336
348
|
account: { type: 'string', description: ACCOUNT_HINT },
|
|
337
349
|
}),
|
|
338
350
|
"email_mark": compileParameters({
|
|
339
|
-
uid: { type: 'integer', required: true, description: '
|
|
351
|
+
uid: { type: ['integer', 'string'], required: true, description: 'IMAP positive integer uid or unchanged AgentMail string message_id from email_list or email_search' },
|
|
340
352
|
action: { type: 'string', required: true, description: 'What to do: read, unread, star, unstar, or move' },
|
|
341
353
|
toFolder: { type: 'string', description: 'Destination folder path for action=move (see email_folders for valid paths)' },
|
|
342
354
|
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
@@ -362,7 +374,7 @@ export const parameters = {
|
|
|
362
374
|
account: { type: 'string', description: ACCOUNT_HINT },
|
|
363
375
|
}),
|
|
364
376
|
"email_reply": compileParameters({
|
|
365
|
-
uid: { type: 'integer', required: true, description: '
|
|
377
|
+
uid: { type: ['integer', 'string'], required: true, description: 'IMAP positive integer uid or unchanged AgentMail string message_id to answer/forward, from email_list or email_search' },
|
|
366
378
|
text: { type: 'string', required: true, description: 'The new text to write; the original is quoted below it automatically' },
|
|
367
379
|
mode: { type: 'string', description: 'reply (default), reply-all, or forward' },
|
|
368
380
|
to: { type: 'string', description: 'Recipient(s) for mode=forward, comma-separated' },
|
|
@@ -376,7 +388,7 @@ export const parameters = {
|
|
|
376
388
|
}),
|
|
377
389
|
"email_health": compileParameters({}),
|
|
378
390
|
"email_attachment": compileParameters({
|
|
379
|
-
uid: { type: 'integer', required: true, description: '
|
|
391
|
+
uid: { type: ['integer', 'string'], required: true, description: 'IMAP positive integer uid or unchanged AgentMail string message_id from email_list or email_search' },
|
|
380
392
|
index: { type: 'integer', description: '0-based attachment index, as listed by email_read; default 0' },
|
|
381
393
|
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
382
394
|
account: { type: 'string', description: ACCOUNT_HINT },
|
package/lib/tools.js
CHANGED
|
@@ -6,6 +6,7 @@ import { descriptions, parameters, MAX_LIMIT, MARK_ACTIONS, REPLY_MODES, executi
|
|
|
6
6
|
/** 单次工具调用的 deadline:普通查询 60s,正文扫描/watch 这类整批操作 120s。 */
|
|
7
7
|
const QUERY_TIMEOUT_MS = 60000;
|
|
8
8
|
const BATCH_TIMEOUT_MS = 120000;
|
|
9
|
+
function validUid(uid) { return typeof uid === 'number' ? Number.isSafeInteger(uid) && uid > 0 : typeof uid === 'string' && uid.length > 0 && uid.length <= 998 && !/[\r\n\0]/.test(uid); }
|
|
9
10
|
function attachmentPaths(value, exec) {
|
|
10
11
|
const context = exec;
|
|
11
12
|
const cwd = context?.agent?.session?.header?.cwd;
|
|
@@ -52,8 +53,8 @@ export function buildEmailTools(runtime, drafts) {
|
|
|
52
53
|
timeoutMs: QUERY_TIMEOUT_MS,
|
|
53
54
|
async execute(rawArgs, exec) {
|
|
54
55
|
const args = rawArgs;
|
|
55
|
-
if (
|
|
56
|
-
throw new Error('uid
|
|
56
|
+
if (!validUid(args.uid)) {
|
|
57
|
+
throw new Error('uid 必须是正整数(IMAP)或 email_list 返回的字符串邮件标识(AgentMail)');
|
|
57
58
|
}
|
|
58
59
|
return await getPool().read(args.account, args.uid, args.folder?.trim() || '', executionSignal(exec));
|
|
59
60
|
}
|
|
@@ -69,8 +70,8 @@ export function buildEmailTools(runtime, drafts) {
|
|
|
69
70
|
timeoutMs: QUERY_TIMEOUT_MS,
|
|
70
71
|
async execute(rawArgs, exec) {
|
|
71
72
|
const args = rawArgs;
|
|
72
|
-
if (
|
|
73
|
-
throw new Error('uid
|
|
73
|
+
if (!validUid(args.uid)) {
|
|
74
|
+
throw new Error('uid 必须是正整数(IMAP)或 email_list 返回的字符串邮件标识(AgentMail)');
|
|
74
75
|
}
|
|
75
76
|
const action = (typeof args.action === 'string' ? args.action.trim().toLowerCase() : '');
|
|
76
77
|
if (!MARK_ACTIONS.includes(action)) {
|
|
@@ -131,8 +132,8 @@ export function buildEmailTools(runtime, drafts) {
|
|
|
131
132
|
timeoutMs: QUERY_TIMEOUT_MS,
|
|
132
133
|
async execute(rawArgs, exec) {
|
|
133
134
|
const args = rawArgs;
|
|
134
|
-
if (
|
|
135
|
-
throw new Error('uid
|
|
135
|
+
if (!validUid(args.uid)) {
|
|
136
|
+
throw new Error('uid 必须是正整数(IMAP)或 email_list 返回的字符串邮件标识(AgentMail)');
|
|
136
137
|
}
|
|
137
138
|
if (typeof args.text !== 'string' || args.text.trim() === '')
|
|
138
139
|
throw new Error('text 不能为空');
|
|
@@ -175,7 +176,7 @@ export function buildEmailTools(runtime, drafts) {
|
|
|
175
176
|
const entries = [...effective.accounts.entries()];
|
|
176
177
|
for (const [accountName, account] of entries.slice(0, 8)) {
|
|
177
178
|
const provider = Object.entries(PROVIDER_PRESETS).find(([, preset]) => (preset.imap.host === account.imap.host && preset.smtp.host === account.smtp.host))?.[0] ?? 'custom';
|
|
178
|
-
const base = provider + ' / ' + account.user + ' / IMAP ' + account.imap.host + ' / SMTP ' + account.smtp.host;
|
|
179
|
+
const base = account.transport === 'agentmail' ? 'AgentMail / ' + account.user + ' / ' + account.apiBase : provider + ' / ' + account.user + ' / IMAP ' + account.imap.host + ' / SMTP ' + account.smtp.host;
|
|
179
180
|
// An OAuth2 account with no token is configured, not broken: the
|
|
180
181
|
// missing piece is a browser login, and saying so is the whole
|
|
181
182
|
// point of this check.
|
|
@@ -214,8 +215,8 @@ export function buildEmailTools(runtime, drafts) {
|
|
|
214
215
|
timeoutMs: QUERY_TIMEOUT_MS,
|
|
215
216
|
async execute(rawArgs, exec) {
|
|
216
217
|
const args = rawArgs;
|
|
217
|
-
if (
|
|
218
|
-
throw new Error('uid
|
|
218
|
+
if (!validUid(args.uid)) {
|
|
219
|
+
throw new Error('uid 必须是正整数(IMAP)或 email_list 返回的字符串邮件标识(AgentMail)');
|
|
219
220
|
}
|
|
220
221
|
const index = clampInt(args.index, 0, 0, 999);
|
|
221
222
|
const workspaceHint = typeof exec?.agent?.session?.header?.cwd === 'string' ? exec.agent.session.header.cwd : undefined;
|
package/lib/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface AddressEntry {
|
|
|
5
5
|
}
|
|
6
6
|
/** One listed message: everything cheap to fetch without the body. */
|
|
7
7
|
export interface ListedMessage {
|
|
8
|
-
uid: number;
|
|
8
|
+
uid: number | string;
|
|
9
9
|
/** ISO 8601 string, or '' when unknown. */
|
|
10
10
|
date: string;
|
|
11
11
|
from: AddressEntry[];
|
|
@@ -36,6 +36,9 @@ export interface ReadMessageBody {
|
|
|
36
36
|
truncated: boolean;
|
|
37
37
|
}
|
|
38
38
|
export interface EmailListResult {
|
|
39
|
+
countExact?: boolean;
|
|
40
|
+
hasMore?: boolean;
|
|
41
|
+
order?: 'relevance';
|
|
39
42
|
account: string;
|
|
40
43
|
count: number;
|
|
41
44
|
folder: string;
|
|
@@ -45,8 +48,9 @@ export interface EmailListResult {
|
|
|
45
48
|
}
|
|
46
49
|
export interface EmailReadResult extends ReadMessageBody {
|
|
47
50
|
account: string;
|
|
48
|
-
uid: number;
|
|
51
|
+
uid: number | string;
|
|
49
52
|
folder: string;
|
|
53
|
+
uidValidity?: number;
|
|
50
54
|
}
|
|
51
55
|
export interface EmailSearchOptions {
|
|
52
56
|
/** Local header window; independent of result offset and bodySearchLimit. */
|
|
@@ -63,6 +67,9 @@ export interface EmailHeaderScan {
|
|
|
63
67
|
nextScanOffset?: number;
|
|
64
68
|
}
|
|
65
69
|
export interface EmailSearchResult {
|
|
70
|
+
countExact?: boolean;
|
|
71
|
+
hasMore?: boolean;
|
|
72
|
+
order?: 'relevance';
|
|
66
73
|
account: string;
|
|
67
74
|
query: string;
|
|
68
75
|
/** Server candidates, local header-window matches, or body-fallback page rows; inspect searchSource and coverage. */
|
|
@@ -75,7 +82,7 @@ export interface EmailSearchResult {
|
|
|
75
82
|
countKind?: 'scanned';
|
|
76
83
|
/** Header rows actually scanned, or the configured body window for legacy renderers. */
|
|
77
84
|
scannedLimit?: number;
|
|
78
|
-
searchSource?: 'server' | 'local-headers' | 'local-body';
|
|
85
|
+
searchSource?: 'server' | 'local-headers' | 'local-body' | 'api';
|
|
79
86
|
serverMatchCount?: number;
|
|
80
87
|
serverSearchFailed?: boolean;
|
|
81
88
|
headerScan?: EmailHeaderScan;
|
|
@@ -103,7 +110,7 @@ export interface EmailFoldersResult {
|
|
|
103
110
|
}
|
|
104
111
|
export interface EmailAttachmentResult {
|
|
105
112
|
account: string;
|
|
106
|
-
uid: number;
|
|
113
|
+
uid: number | string;
|
|
107
114
|
filename: string;
|
|
108
115
|
contentType: string;
|
|
109
116
|
size: number;
|
|
@@ -123,7 +130,7 @@ export interface EmailListArgs extends AccountArg {
|
|
|
123
130
|
until?: string;
|
|
124
131
|
}
|
|
125
132
|
export interface EmailReadArgs extends AccountArg {
|
|
126
|
-
uid: number;
|
|
133
|
+
uid: number | string;
|
|
127
134
|
folder?: string;
|
|
128
135
|
}
|
|
129
136
|
export interface EmailSearchArgs extends AccountArg {
|
|
@@ -148,7 +155,7 @@ export interface EmailFoldersArgs extends AccountArg {
|
|
|
148
155
|
subscribedOnly?: boolean;
|
|
149
156
|
}
|
|
150
157
|
export interface EmailAttachmentArgs extends AccountArg {
|
|
151
|
-
uid: number;
|
|
158
|
+
uid: number | string;
|
|
152
159
|
/** 0-based index into the attachments of email_read. Default 0. */
|
|
153
160
|
index?: number;
|
|
154
161
|
folder?: string;
|
|
@@ -160,7 +167,7 @@ export interface EmailWatchArgs extends AccountArg {
|
|
|
160
167
|
}
|
|
161
168
|
export type EmailMarkAction = 'read' | 'unread' | 'star' | 'unstar' | 'move';
|
|
162
169
|
export interface EmailMarkArgs extends AccountArg {
|
|
163
|
-
uid: number;
|
|
170
|
+
uid: number | string;
|
|
164
171
|
action: EmailMarkAction;
|
|
165
172
|
/** Target folder path; only used (and required) when action is 'move'. */
|
|
166
173
|
toFolder?: string;
|
|
@@ -168,7 +175,7 @@ export interface EmailMarkArgs extends AccountArg {
|
|
|
168
175
|
}
|
|
169
176
|
export interface EmailMarkResult {
|
|
170
177
|
account: string;
|
|
171
|
-
uid: number;
|
|
178
|
+
uid: number | string;
|
|
172
179
|
folder: string;
|
|
173
180
|
action: EmailMarkAction;
|
|
174
181
|
seen: boolean;
|
|
@@ -180,7 +187,7 @@ export interface EmailMarkResult {
|
|
|
180
187
|
}
|
|
181
188
|
export type EmailReplyMode = 'reply' | 'reply-all' | 'forward';
|
|
182
189
|
export interface EmailReplyArgs extends AccountArg {
|
|
183
|
-
uid: number;
|
|
190
|
+
uid: number | string;
|
|
184
191
|
/** The new text to write; the original is quoted below it automatically. */
|
|
185
192
|
text: string;
|
|
186
193
|
mode?: EmailReplyMode;
|
|
@@ -193,7 +200,7 @@ export interface EmailReplyResult {
|
|
|
193
200
|
account: string;
|
|
194
201
|
mode: EmailReplyMode;
|
|
195
202
|
/** uid of the original message that was answered/forwarded. */
|
|
196
|
-
originalUid: number;
|
|
203
|
+
originalUid: number | string;
|
|
197
204
|
messageId: string;
|
|
198
205
|
accepted: string[];
|
|
199
206
|
rejected: string[];
|
|
@@ -202,6 +209,7 @@ export interface EmailReplyResult {
|
|
|
202
209
|
subject: string;
|
|
203
210
|
}
|
|
204
211
|
export interface EmailWatchResult {
|
|
212
|
+
totalUnreadExact?: boolean;
|
|
205
213
|
account: string;
|
|
206
214
|
folder: string;
|
|
207
215
|
/** True on the first call for this account+folder: it sets the baseline. */
|
package/lib/web.d.ts
CHANGED
|
@@ -175,7 +175,8 @@ export declare class EmailSettingsBackend {
|
|
|
175
175
|
private readonly ctx;
|
|
176
176
|
private readonly scope;
|
|
177
177
|
private readonly rowConfig;
|
|
178
|
-
|
|
178
|
+
private readonly apiFetch?;
|
|
179
|
+
constructor(ctx: any, scope: any, rowConfig: EmailConfig, apiFetch?: typeof fetch | undefined);
|
|
179
180
|
/** Wired by apply(): the email_watch core; 'web' keeps its own cursor scope. */
|
|
180
181
|
watchImpl?: (account: string, folder: string, limit: number, scope: string) => Promise<EmailWatchResult>;
|
|
181
182
|
private get namespace();
|
|
@@ -262,11 +263,18 @@ export declare class EmailSettingsBackend {
|
|
|
262
263
|
* what was actually tried — including on failure.
|
|
263
264
|
*/
|
|
264
265
|
test(value: EmailSettingsValue, accountName?: string): Promise<{
|
|
266
|
+
ok: boolean;
|
|
267
|
+
account: string;
|
|
268
|
+
apiHost: "https://api.agentmail.to" | "https://api.agentmail.eu" | undefined;
|
|
269
|
+
message: string;
|
|
270
|
+
} | {
|
|
265
271
|
account: string;
|
|
266
272
|
imapHost: string;
|
|
267
273
|
imapPort: number;
|
|
268
274
|
ok: boolean;
|
|
269
275
|
ms: number;
|
|
276
|
+
apiHost?: undefined;
|
|
277
|
+
message?: undefined;
|
|
270
278
|
}>;
|
|
271
279
|
/**
|
|
272
280
|
* Resolve one named account of the *stored* settings — the same accounts the
|
package/lib/web.js
CHANGED
|
@@ -6,6 +6,7 @@ import { SETTINGS_NAMESPACE, toEmailConfig, validateSettingsValue } from './sett
|
|
|
6
6
|
import { isOAuth2Account, OUTLOOK_OAUTH2_CLIENT_ID, parseAccountsYaml, parseServerPresets, presetNamesIn, PROVIDER_PRESETS, resolveEmailSettings, serializeAccountsYaml, } from './config.js';
|
|
7
7
|
import { clearTokenFor, getFreshAccessToken, oauth2StateOf, pollDeviceFlow, startDeviceFlow, } from './oauth2.js';
|
|
8
8
|
import { EmailPool, messageOf, redactCredentials } from './mail-client.js';
|
|
9
|
+
import { AgentMailClient } from './agentmail.js';
|
|
9
10
|
/** Same-origin route the browser settings section talks to. */
|
|
10
11
|
export const SETTINGS_ROUTE = '/_dsh/dsh-email/settings';
|
|
11
12
|
/** Same-origin route serving the whale-girl courier image to the widget. */
|
|
@@ -98,7 +99,7 @@ function buildAccountCards(raw, defaultAccount, presets, tokens = () => ({ state
|
|
|
98
99
|
provider: providerName,
|
|
99
100
|
...(preset?.label !== undefined && preset.label !== '' ? { providerLabel: preset.label } : {}),
|
|
100
101
|
user,
|
|
101
|
-
hasPassword: typeof account.password === 'string' && account.password !== '',
|
|
102
|
+
hasPassword: typeof account.password === 'string' && account.password !== '' || typeof account.apiKey === 'string' && account.apiKey !== '',
|
|
102
103
|
authKind,
|
|
103
104
|
...(pinned === 'oauth2' || pinned === 'password' ? { authKindDeclared: pinned } : {}),
|
|
104
105
|
...(clientId !== '' ? { clientId } : {}),
|
|
@@ -308,6 +309,8 @@ function washEndpoint(account, key) {
|
|
|
308
309
|
function persistedProvider(provider, customNames) {
|
|
309
310
|
if (typeof provider !== 'string' || provider === '')
|
|
310
311
|
return undefined;
|
|
312
|
+
if (provider === 'agentmail' || provider === 'agentmail-eu')
|
|
313
|
+
return provider;
|
|
311
314
|
if (Object.prototype.hasOwnProperty.call(PROVIDER_PRESETS, provider))
|
|
312
315
|
return provider;
|
|
313
316
|
return customNames.has(provider) ? provider : undefined;
|
|
@@ -520,6 +523,15 @@ function serializeAccountsDraft(source, cards, defaultAccount, customNames) {
|
|
|
520
523
|
// never written — a stored copy is washed out below instead.
|
|
521
524
|
const previousProvider = account.get('provider');
|
|
522
525
|
const nextProvider = persistedProvider(card.provider, customNames);
|
|
526
|
+
const apiProvider = (value) => value === 'agentmail' || value === 'agentmail-eu';
|
|
527
|
+
if (apiProvider(previousProvider) !== apiProvider(nextProvider)) {
|
|
528
|
+
account.delete('password');
|
|
529
|
+
account.delete('apiKey');
|
|
530
|
+
account.delete('authPassword');
|
|
531
|
+
account.delete('authKind');
|
|
532
|
+
}
|
|
533
|
+
if (card.password !== undefined && apiProvider(nextProvider))
|
|
534
|
+
account.delete('apiKey');
|
|
523
535
|
writeField(account, 'provider', nextProvider);
|
|
524
536
|
writeField(account, 'user', card.user);
|
|
525
537
|
// senderName / authUser are plain three-state fields (undefined = 保持原样,
|
|
@@ -749,10 +761,12 @@ export class EmailSettingsBackend {
|
|
|
749
761
|
ctx;
|
|
750
762
|
scope;
|
|
751
763
|
rowConfig;
|
|
752
|
-
|
|
764
|
+
apiFetch;
|
|
765
|
+
constructor(ctx, scope, rowConfig, apiFetch) {
|
|
753
766
|
this.ctx = ctx;
|
|
754
767
|
this.scope = scope;
|
|
755
768
|
this.rowConfig = rowConfig;
|
|
769
|
+
this.apiFetch = apiFetch;
|
|
756
770
|
}
|
|
757
771
|
/** Wired by apply(): the email_watch core; 'web' keeps its own cursor scope. */
|
|
758
772
|
watchImpl;
|
|
@@ -886,6 +900,10 @@ export class EmailSettingsBackend {
|
|
|
886
900
|
if (cfg === undefined) {
|
|
887
901
|
throw new Error(`未知账号 "${name}",可用:${available.join('、')}`);
|
|
888
902
|
}
|
|
903
|
+
if (cfg.transport === 'agentmail') {
|
|
904
|
+
await new AgentMailClient(settings, this.apiFetch).test(name);
|
|
905
|
+
return { ok: true, account: name, apiHost: cfg.apiBase, message: 'AgentMail API 连接成功 / AgentMail API connection successful' };
|
|
906
|
+
}
|
|
889
907
|
const target = { account: name, imapHost: cfg.imap.host, imapPort: cfg.imap.port };
|
|
890
908
|
const targetDescription = '账号 "' + name + '" · IMAP ' + cfg.imap.host + ':' + cfg.imap.port;
|
|
891
909
|
// An OAuth2 account has no password to check: without a token there is
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-email",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "在 DSH
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"description": "在 DSH 中浏览、收发、搜索和整理邮件,支持 IMAP/SMTP 与 AgentMail 多账号。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"dsh",
|
|
35
35
|
"email",
|
|
36
36
|
"imap",
|
|
37
|
-
"smtp"
|
|
37
|
+
"smtp",
|
|
38
|
+
"agentmail"
|
|
38
39
|
],
|
|
39
40
|
"author": "stardustlc",
|
|
40
41
|
"repository": {
|