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.
@@ -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
- type: string;
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
- type: string;
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
- type: string;
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
- type: string;
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
- type: string;
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
- type: string;
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
- type: string;
395
+ oneOf: {
396
+ type: string;
397
+ }[];
369
398
  };
370
399
  date: {
371
400
  type: string;
@@ -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 + ':已建立新邮件监视基线(当前未读 ' + value.totalUnread + ' 封)。之后调用 email_watch 只会报告新到的邮件。');
276
+ return oneText('账号 ' + value.account + ':已建立新邮件监视基线(' + unread + ')。之后调用 email_watch 只会报告新到的邮件。');
266
277
  }
267
278
  if (value.newCount === 0) {
268
- return oneText('账号 ' + value.account + ':没有新邮件(当前未读 ' + value.totalUnread + ' 封)。');
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: 'Message uid from email_list or email_search' },
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: 'Message uid from email_list or email_search' },
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: 'Message uid to answer/forward, from email_list or email_search' },
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: 'Message uid from email_list or email_search' },
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 (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
56
- throw new Error('uid 必须是正整数(用 email_list 获取)');
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 (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
73
- throw new Error('uid 必须是正整数(用 email_list 获取)');
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 (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
135
- throw new Error('uid 必须是正整数(用 email_list 获取)');
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 (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
218
- throw new Error('uid 必须是正整数(用 email_list 获取)');
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
- constructor(ctx: any, scope: any, rowConfig: EmailConfig);
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
- constructor(ctx, scope, rowConfig) {
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.17.3",
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": {