dsh-email 0.9.1 → 0.10.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/README.md +2 -0
- package/lib/index.js +61 -0
- package/lib/mail-client.d.ts +3 -2
- package/lib/mail-client.js +58 -6
- package/lib/types.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Email tools for DeepSeek Harness: list, read, search and send mail through stand
|
|
|
29
29
|
| `email_folders` | 列出邮箱的文件夹(INBOX/已发送/垃圾邮件/自定义…),拿 path 喂给其他工具 |
|
|
30
30
|
| `email_attachment` | 按序号下载邮件附件(默认存到会话工作区,模型可直接读取;大小受 maxAttachmentBytes 限制) |
|
|
31
31
|
| `email_watch` | 增量检查新邮件:首次调用建立基线,之后每次只报告比上次多出来的未读邮件,适合定时任务做新邮件提醒 |
|
|
32
|
+
| `email_mark` | 修改邮件状态:标记已读/未读、加/取消星标,或移动到别的文件夹(归档、丢回收站),收发闭环的「收完之后」那一半 |
|
|
32
33
|
|
|
33
34
|
### 新邮件提醒(Web 端)
|
|
34
35
|
|
|
@@ -42,6 +43,7 @@ Email tools for DeepSeek Harness: list, read, search and send mail through stand
|
|
|
42
43
|
|
|
43
44
|
### 版本记录
|
|
44
45
|
|
|
46
|
+
- **0.10.0**:新增 `email_mark` 工具——标记已读/未读、加/取消星标、移动文件夹(归档/整理),补齐收发闭环;连接池按读/写模式分别管理邮箱打开状态。
|
|
45
47
|
- **0.9.1**:修复设置页空主机遮蔽 provider 预设(#3/#6);IMAP 连接超时不再杀死整个 DSH 进程(#4);暗色模式输入控件可见(#2);密码栏提示环境变量 `DSH_EMAIL_PASSWORD` 免明文方案(#5)。
|
|
46
48
|
- **0.9.0**:新增 `email_watch` 增量新邮件检查工具(游标式,适合定时提醒);Web 端新增「鲸鱼娘递信」新邮件弹窗(本地皮肤素材运行时读取 + 内置回退图)。
|
|
47
49
|
- **0.8.2**:`since` / `until` 参数描述与其余参数统一为英文,方便多语言 agent 理解。
|
package/lib/index.js
CHANGED
|
@@ -137,6 +137,21 @@ const attachmentSchema = {
|
|
|
137
137
|
},
|
|
138
138
|
additionalProperties: true,
|
|
139
139
|
};
|
|
140
|
+
const markSchema = {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
account: { type: 'string' },
|
|
144
|
+
uid: { type: 'integer' },
|
|
145
|
+
folder: { type: 'string' },
|
|
146
|
+
action: { type: 'string' },
|
|
147
|
+
seen: { type: 'boolean' },
|
|
148
|
+
flagged: { type: 'boolean' },
|
|
149
|
+
movedTo: { type: 'string' },
|
|
150
|
+
movedUid: { type: 'integer' },
|
|
151
|
+
},
|
|
152
|
+
additionalProperties: true,
|
|
153
|
+
};
|
|
154
|
+
const MARK_ACTIONS = ['read', 'unread', 'star', 'unstar', 'move'];
|
|
140
155
|
function oneText(text) {
|
|
141
156
|
return [{ type: 'text', text }];
|
|
142
157
|
}
|
|
@@ -194,6 +209,23 @@ function renderFolders(value) {
|
|
|
194
209
|
function renderAttachment(value) {
|
|
195
210
|
return oneText('账号 ' + value.account + ' 已下载附件 "' + value.filename + '"(' + value.contentType + ',' + value.size + ' 字节)到:\n' + value.path + '\n可用 read 工具读取该文件。');
|
|
196
211
|
}
|
|
212
|
+
const MARK_LABELS = {
|
|
213
|
+
read: '标记为已读',
|
|
214
|
+
unread: '标记为未读',
|
|
215
|
+
star: '加星标',
|
|
216
|
+
unstar: '取消星标',
|
|
217
|
+
move: '移动',
|
|
218
|
+
};
|
|
219
|
+
function renderMark(value) {
|
|
220
|
+
let text = '账号 ' + value.account + ':文件夹 "' + value.folder + '" 中 uid=' + value.uid + ' 已' + (MARK_LABELS[value.action] ?? value.action);
|
|
221
|
+
if (value.action === 'move') {
|
|
222
|
+
text += '到 "' + (value.movedTo ?? '') + '"' + (typeof value.movedUid === 'number' ? '(新 uid=' + value.movedUid + ')' : '');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
text += '(当前:' + (value.seen ? '已读' : '未读') + (value.flagged ? '、已标星' : '') + ')';
|
|
226
|
+
}
|
|
227
|
+
return oneText(text);
|
|
228
|
+
}
|
|
197
229
|
function fingerprintSettings(settings) {
|
|
198
230
|
return JSON.stringify({
|
|
199
231
|
accounts: [...settings.accounts.entries()].map(([name, account]) => [name, account]),
|
|
@@ -293,6 +325,35 @@ export function apply(ctx, config = {}) {
|
|
|
293
325
|
return await getPool().read(args.account, args.uid, args.folder?.trim() || '');
|
|
294
326
|
},
|
|
295
327
|
});
|
|
328
|
+
ctx.tools.register({
|
|
329
|
+
name: 'email_mark',
|
|
330
|
+
description: '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.',
|
|
331
|
+
parameters: compileParameters({
|
|
332
|
+
uid: { type: 'integer', required: true, description: 'Message uid from email_list or email_search' },
|
|
333
|
+
action: { type: 'string', required: true, description: 'What to do: read, unread, star, unstar, or move' },
|
|
334
|
+
toFolder: { type: 'string', description: 'Destination folder path for action=move (see email_folders for valid paths)' },
|
|
335
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
336
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
337
|
+
}),
|
|
338
|
+
output: {
|
|
339
|
+
schema: markSchema,
|
|
340
|
+
render: (_args, value) => renderMark(value),
|
|
341
|
+
},
|
|
342
|
+
async execute(rawArgs) {
|
|
343
|
+
const args = rawArgs;
|
|
344
|
+
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
345
|
+
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
346
|
+
}
|
|
347
|
+
const action = (typeof args.action === 'string' ? args.action.trim().toLowerCase() : '');
|
|
348
|
+
if (!MARK_ACTIONS.includes(action)) {
|
|
349
|
+
throw new Error('action 必须是 ' + MARK_ACTIONS.join('、') + ' 之一');
|
|
350
|
+
}
|
|
351
|
+
if (action === 'move' && (typeof args.toFolder !== 'string' || args.toFolder.trim() === '')) {
|
|
352
|
+
throw new Error('action=move 时需要 toFolder 参数(用 email_folders 查看可用文件夹)');
|
|
353
|
+
}
|
|
354
|
+
return await getPool().mark(args.account, args.folder?.trim() || '', args.uid, action, args.toFolder);
|
|
355
|
+
},
|
|
356
|
+
});
|
|
296
357
|
ctx.tools.register({
|
|
297
358
|
name: 'email_search',
|
|
298
359
|
description: 'Search emails by a keyword matched against sender, recipient and subject (server-side IMAP SEARCH). Body search is not supported by every server and is not attempted; returns the same compact rows as email_list.',
|
package/lib/mail-client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ImapFlow } from 'imapflow';
|
|
2
2
|
import type { ResolvedEmailConfig, ResolvedEmailSettings } from './config.js';
|
|
3
|
-
import type { EmailAttachmentMeta, EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailReadResult, EmailSearchResult, EmailSendResult } from './types.js';
|
|
3
|
+
import type { EmailAttachmentMeta, EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailMarkAction, EmailMarkResult, EmailReadResult, EmailSearchResult, EmailSendResult } from './types.js';
|
|
4
4
|
export declare class MailError extends Error {
|
|
5
5
|
constructor(message: string);
|
|
6
6
|
}
|
|
@@ -35,7 +35,7 @@ export declare class EmailPool {
|
|
|
35
35
|
resolveName(name?: string): string;
|
|
36
36
|
/** Serialize operations per account: one IMAP connection serves one op at a time. */
|
|
37
37
|
private enqueue;
|
|
38
|
-
withImap<T>(accountName: string | undefined, folder: string | null, run: (client: ImapFlow) => Promise<T
|
|
38
|
+
withImap<T>(accountName: string | undefined, folder: string | null, run: (client: ImapFlow) => Promise<T>, readOnly?: boolean): Promise<T>;
|
|
39
39
|
private createImap;
|
|
40
40
|
private imapRun;
|
|
41
41
|
private normalizeImapError;
|
|
@@ -50,6 +50,7 @@ export declare class EmailPool {
|
|
|
50
50
|
private searchBodies;
|
|
51
51
|
private fetchListed;
|
|
52
52
|
read(accountName: string | undefined, uid: number, folder: string): Promise<EmailReadResult>;
|
|
53
|
+
mark(accountName: string | undefined, folder: string, uid: number, action: EmailMarkAction, toFolder?: string): Promise<EmailMarkResult>;
|
|
53
54
|
folders(accountName: string | undefined, subscribedOnly: boolean): Promise<EmailFoldersResult>;
|
|
54
55
|
downloadAttachment(accountName: string | undefined, folder: string, uid: number, index: number, workspaceHint?: string): Promise<EmailAttachmentResult>;
|
|
55
56
|
send(accountName: string | undefined, to: string, subject: string, text: string | undefined, cc: string | undefined, attachmentPaths: string[] | undefined): Promise<EmailSendResult>;
|
package/lib/mail-client.js
CHANGED
|
@@ -114,10 +114,10 @@ export class EmailPool {
|
|
|
114
114
|
this.queues.set(name, next.then(() => undefined, () => undefined));
|
|
115
115
|
return next;
|
|
116
116
|
}
|
|
117
|
-
async withImap(accountName, folder, run) {
|
|
117
|
+
async withImap(accountName, folder, run, readOnly = true) {
|
|
118
118
|
const name = this.resolveName(accountName);
|
|
119
119
|
const cfg = this.account(name);
|
|
120
|
-
return this.enqueue(name, () => this.imapRun(name, cfg, folder, run));
|
|
120
|
+
return this.enqueue(name, () => this.imapRun(name, cfg, folder, readOnly, run));
|
|
121
121
|
}
|
|
122
122
|
createImap(cfg) {
|
|
123
123
|
const client = new ImapFlow({
|
|
@@ -144,7 +144,7 @@ export class EmailPool {
|
|
|
144
144
|
});
|
|
145
145
|
return client;
|
|
146
146
|
}
|
|
147
|
-
async imapRun(name, cfg, folder, run) {
|
|
147
|
+
async imapRun(name, cfg, folder, readOnly, run) {
|
|
148
148
|
let entry = this.imaps.get(name);
|
|
149
149
|
try {
|
|
150
150
|
if (entry === undefined || !entry.client.usable) {
|
|
@@ -152,14 +152,17 @@ export class EmailPool {
|
|
|
152
152
|
await this.evictImap(name);
|
|
153
153
|
const client = this.createImap(cfg);
|
|
154
154
|
await client.connect();
|
|
155
|
-
entry = { client, selected: null, lastUsed: Date.now(), inUse: 0 };
|
|
155
|
+
entry = { client, selected: null, selectedReadOnly: true, lastUsed: Date.now(), inUse: 0 };
|
|
156
156
|
this.imaps.set(name, entry);
|
|
157
157
|
}
|
|
158
158
|
entry.lastUsed = Date.now();
|
|
159
159
|
entry.inUse += 1;
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// Reopen when the folder changes or when the caller needs a different
|
|
161
|
+
// access mode (email_mark writes flags / moves messages).
|
|
162
|
+
if (folder !== null && (entry.selected !== folder || entry.selectedReadOnly !== readOnly)) {
|
|
163
|
+
await entry.client.mailboxOpen(folder, { readOnly });
|
|
162
164
|
entry.selected = folder;
|
|
165
|
+
entry.selectedReadOnly = readOnly;
|
|
163
166
|
}
|
|
164
167
|
const result = await run(entry.client);
|
|
165
168
|
entry.lastUsed = Date.now();
|
|
@@ -355,6 +358,55 @@ export class EmailPool {
|
|
|
355
358
|
return { account: name, uid, folder: folderName, ...body };
|
|
356
359
|
});
|
|
357
360
|
}
|
|
361
|
+
async mark(accountName, folder, uid, action, toFolder) {
|
|
362
|
+
const name = this.resolveName(accountName);
|
|
363
|
+
const cfg = this.account(name);
|
|
364
|
+
const folderName = folder || cfg.inboxFolder;
|
|
365
|
+
return this.withImap(name, folderName, async (client) => {
|
|
366
|
+
const before = await client.fetchOne(uid, { uid: true, flags: true }, { uid: true });
|
|
367
|
+
if (before === false) {
|
|
368
|
+
throw new MailError('找不到 uid=' + uid + ' 的邮件(可能已被删除,或不在文件夹 "' + folderName + '";可用 email_list 重新获取 uid)');
|
|
369
|
+
}
|
|
370
|
+
let seen = before.flags?.has('\\Seen') === true;
|
|
371
|
+
let flagged = before.flags?.has('\\Flagged') === true;
|
|
372
|
+
if (action === 'read' && !seen) {
|
|
373
|
+
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
|
|
374
|
+
seen = true;
|
|
375
|
+
}
|
|
376
|
+
else if (action === 'unread' && seen) {
|
|
377
|
+
await client.messageFlagsRemove(uid, ['\\Seen'], { uid: true });
|
|
378
|
+
seen = false;
|
|
379
|
+
}
|
|
380
|
+
else if (action === 'star' && !flagged) {
|
|
381
|
+
await client.messageFlagsAdd(uid, ['\\Flagged'], { uid: true });
|
|
382
|
+
flagged = true;
|
|
383
|
+
}
|
|
384
|
+
else if (action === 'unstar' && flagged) {
|
|
385
|
+
await client.messageFlagsRemove(uid, ['\\Flagged'], { uid: true });
|
|
386
|
+
flagged = false;
|
|
387
|
+
}
|
|
388
|
+
else if (action === 'move') {
|
|
389
|
+
const target = (toFolder ?? '').trim();
|
|
390
|
+
if (target === '')
|
|
391
|
+
throw new MailError('move 操作需要 toFolder 参数(用 email_folders 查看可用文件夹)');
|
|
392
|
+
if (target === folderName)
|
|
393
|
+
throw new MailError('邮件已在文件夹 "' + folderName + '" 中,无需移动');
|
|
394
|
+
const folders = await client.list();
|
|
395
|
+
if (!folders.some(row => row.path === target)) {
|
|
396
|
+
throw new MailError('找不到目标文件夹 "' + target + '",可用:' + folders.map(row => row.path).join('、'));
|
|
397
|
+
}
|
|
398
|
+
const moved = await client.messageMove(uid, target, { uid: true });
|
|
399
|
+
if (moved === false)
|
|
400
|
+
throw new MailError('移动 uid=' + uid + ' 到 "' + target + '" 失败(服务器拒绝了 MOVE/COPY)');
|
|
401
|
+
const result = { account: name, uid, folder: folderName, action, seen, flagged, movedTo: target };
|
|
402
|
+
const destUid = moved?.destinationUid;
|
|
403
|
+
if (typeof destUid === 'number')
|
|
404
|
+
result.movedUid = destUid;
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
return { account: name, uid, folder: folderName, action, seen, flagged };
|
|
408
|
+
}, false);
|
|
409
|
+
}
|
|
358
410
|
async folders(accountName, subscribedOnly) {
|
|
359
411
|
const name = this.resolveName(accountName);
|
|
360
412
|
return this.withImap(name, null, async (client) => {
|
package/lib/types.d.ts
CHANGED
|
@@ -124,6 +124,26 @@ export interface EmailWatchArgs extends AccountArg {
|
|
|
124
124
|
/** Max number of new messages to return per call, default 20. */
|
|
125
125
|
limit?: number;
|
|
126
126
|
}
|
|
127
|
+
export type EmailMarkAction = 'read' | 'unread' | 'star' | 'unstar' | 'move';
|
|
128
|
+
export interface EmailMarkArgs extends AccountArg {
|
|
129
|
+
uid: number;
|
|
130
|
+
action: EmailMarkAction;
|
|
131
|
+
/** Target folder path; only used (and required) when action is 'move'. */
|
|
132
|
+
toFolder?: string;
|
|
133
|
+
folder?: string;
|
|
134
|
+
}
|
|
135
|
+
export interface EmailMarkResult {
|
|
136
|
+
account: string;
|
|
137
|
+
uid: number;
|
|
138
|
+
folder: string;
|
|
139
|
+
action: EmailMarkAction;
|
|
140
|
+
seen: boolean;
|
|
141
|
+
flagged: boolean;
|
|
142
|
+
/** Set after a successful move: the destination folder path. */
|
|
143
|
+
movedTo?: string;
|
|
144
|
+
/** Set after a successful move: uid in the destination when the server reports it. */
|
|
145
|
+
movedUid?: number;
|
|
146
|
+
}
|
|
127
147
|
export interface EmailWatchResult {
|
|
128
148
|
account: string;
|
|
129
149
|
folder: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|