dsh-mail-sender 0.1.3 → 0.1.5
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/lib/index.d.ts +19 -67
- package/lib/index.js +96 -65
- package/package.json +7 -2
- package/readme.md +9 -49
package/lib/index.d.ts
CHANGED
|
@@ -7,78 +7,30 @@ interface Ref<T> {
|
|
|
7
7
|
get(): T;
|
|
8
8
|
}
|
|
9
9
|
export interface Config {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
port: Ref<number>;
|
|
20
|
-
secure: Ref<boolean>;
|
|
21
|
-
user: Ref<string>;
|
|
22
|
-
password: Ref<string>;
|
|
23
|
-
};
|
|
24
|
-
/** 入站+出站合并白名单 */
|
|
10
|
+
/** 机器人邮箱,同时用于 IMAP/SMTP 登录与回信 From */
|
|
11
|
+
email: Ref<string>;
|
|
12
|
+
/** 邮箱密码或授权码 */
|
|
13
|
+
password: Ref<string>;
|
|
14
|
+
imapHost: Ref<string>;
|
|
15
|
+
smtpHost: Ref<string>;
|
|
16
|
+
/** SMTP 端口,465 走 SSL,其余走 STARTTLS */
|
|
17
|
+
smtpPort: Ref<number>;
|
|
18
|
+
/** 入站+出站白名单 */
|
|
25
19
|
allowedAddresses: Ref<string[]>;
|
|
26
20
|
}
|
|
27
21
|
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
password: Schema<string, string, "volatile-defined">;
|
|
34
|
-
}>>, Schemastery.ObjectT<NoInfer<{
|
|
35
|
-
host: Schema<string, string, "volatile-defined">;
|
|
36
|
-
port: Schema<number, number, "volatile-defined">;
|
|
37
|
-
secure: Schema<boolean, boolean, "volatile-defined">;
|
|
38
|
-
user: Schema<string, string, "volatile-defined">;
|
|
39
|
-
password: Schema<string, string, "volatile-defined">;
|
|
40
|
-
}>>, "plain">;
|
|
41
|
-
smtp: Schema<Schemastery.ObjectS<NoInfer<{
|
|
42
|
-
host: Schema<string, string, "volatile-defined">;
|
|
43
|
-
port: Schema<number, number, "volatile-defined">;
|
|
44
|
-
secure: Schema<boolean, boolean, "volatile-defined">;
|
|
45
|
-
user: Schema<string, string, "volatile-defined">;
|
|
46
|
-
password: Schema<string, string, "volatile-defined">;
|
|
47
|
-
}>>, Schemastery.ObjectT<NoInfer<{
|
|
48
|
-
host: Schema<string, string, "volatile-defined">;
|
|
49
|
-
port: Schema<number, number, "volatile-defined">;
|
|
50
|
-
secure: Schema<boolean, boolean, "volatile-defined">;
|
|
51
|
-
user: Schema<string, string, "volatile-defined">;
|
|
52
|
-
password: Schema<string, string, "volatile-defined">;
|
|
53
|
-
}>>, "plain">;
|
|
22
|
+
email: Schema<string, string, "volatile-defined">;
|
|
23
|
+
password: Schema<string, string, "volatile-defined">;
|
|
24
|
+
imapHost: Schema<string, string, "volatile-defined">;
|
|
25
|
+
smtpHost: Schema<string, string, "volatile-defined">;
|
|
26
|
+
smtpPort: Schema<number, number, "volatile-defined">;
|
|
54
27
|
allowedAddresses: Schema<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
55
28
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
password: Schema<string, string, "volatile-defined">;
|
|
62
|
-
}>>, Schemastery.ObjectT<NoInfer<{
|
|
63
|
-
host: Schema<string, string, "volatile-defined">;
|
|
64
|
-
port: Schema<number, number, "volatile-defined">;
|
|
65
|
-
secure: Schema<boolean, boolean, "volatile-defined">;
|
|
66
|
-
user: Schema<string, string, "volatile-defined">;
|
|
67
|
-
password: Schema<string, string, "volatile-defined">;
|
|
68
|
-
}>>, "plain">;
|
|
69
|
-
smtp: Schema<Schemastery.ObjectS<NoInfer<{
|
|
70
|
-
host: Schema<string, string, "volatile-defined">;
|
|
71
|
-
port: Schema<number, number, "volatile-defined">;
|
|
72
|
-
secure: Schema<boolean, boolean, "volatile-defined">;
|
|
73
|
-
user: Schema<string, string, "volatile-defined">;
|
|
74
|
-
password: Schema<string, string, "volatile-defined">;
|
|
75
|
-
}>>, Schemastery.ObjectT<NoInfer<{
|
|
76
|
-
host: Schema<string, string, "volatile-defined">;
|
|
77
|
-
port: Schema<number, number, "volatile-defined">;
|
|
78
|
-
secure: Schema<boolean, boolean, "volatile-defined">;
|
|
79
|
-
user: Schema<string, string, "volatile-defined">;
|
|
80
|
-
password: Schema<string, string, "volatile-defined">;
|
|
81
|
-
}>>, "plain">;
|
|
29
|
+
email: Schema<string, string, "volatile-defined">;
|
|
30
|
+
password: Schema<string, string, "volatile-defined">;
|
|
31
|
+
imapHost: Schema<string, string, "volatile-defined">;
|
|
32
|
+
smtpHost: Schema<string, string, "volatile-defined">;
|
|
33
|
+
smtpPort: Schema<number, number, "volatile-defined">;
|
|
82
34
|
allowedAddresses: Schema<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
83
35
|
}>>, "plain">;
|
|
84
36
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -6,65 +6,77 @@ import Schema from '@deepseek-ai/schemastery';
|
|
|
6
6
|
export const name = 'dsh-mail-sender';
|
|
7
7
|
export const inject = ['agents'];
|
|
8
8
|
export const Config = Schema.object({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
password: Schema.string().volatile().default('').description('IMAP 密码/授权码'),
|
|
15
|
-
}).description('IMAP 收信'),
|
|
16
|
-
smtp: Schema.object({
|
|
17
|
-
host: Schema.string().volatile().default('').description('SMTP 服务器地址'),
|
|
18
|
-
port: Schema.number().volatile().default(465),
|
|
19
|
-
secure: Schema.boolean().volatile().default(true),
|
|
20
|
-
user: Schema.string().volatile().default('').description('SMTP 登录账号'),
|
|
21
|
-
password: Schema.string().volatile().default('').description('SMTP 密码/授权码'),
|
|
22
|
-
}).description('SMTP 发信'),
|
|
9
|
+
email: Schema.string().volatile().default('').description('机器人邮箱,如 agent@qq.com'),
|
|
10
|
+
password: Schema.string().volatile().default('').description('邮箱密码或授权码'),
|
|
11
|
+
imapHost: Schema.string().volatile().default('').description('IMAP 服务器,如 imap.qq.com'),
|
|
12
|
+
smtpHost: Schema.string().volatile().default('').description('SMTP 服务器,如 smtp.qq.com'),
|
|
13
|
+
smtpPort: Schema.number().volatile().default(465).description('SMTP 端口,465 走 SSL,587 走 STARTTLS'),
|
|
23
14
|
allowedAddresses: Schema.array(Schema.string())
|
|
24
15
|
.volatile()
|
|
25
16
|
.default([])
|
|
26
17
|
.description('允许与 agent 通信的邮箱地址'),
|
|
27
18
|
});
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let smtpKey = '';
|
|
39
|
-
const getSmtp = () => {
|
|
40
|
-
const conf = {
|
|
41
|
-
host: config.smtp.host.get(),
|
|
42
|
-
port: config.smtp.port.get(),
|
|
43
|
-
secure: config.smtp.secure.get(),
|
|
44
|
-
auth: { user: config.smtp.user.get(), pass: config.smtp.password.get() },
|
|
45
|
-
};
|
|
46
|
-
const key = JSON.stringify(conf);
|
|
47
|
-
if (smtpKey !== key) {
|
|
48
|
-
smtp = nodemailer.createTransport(conf);
|
|
49
|
-
smtpKey = key;
|
|
50
|
-
}
|
|
51
|
-
return smtp;
|
|
19
|
+
function resolveServers(config) {
|
|
20
|
+
const smtpPort = config.smtpPort.get();
|
|
21
|
+
return {
|
|
22
|
+
email: config.email.get().trim(),
|
|
23
|
+
password: config.password.get(),
|
|
24
|
+
imapHost: config.imapHost.get().trim(),
|
|
25
|
+
imapPort: 993,
|
|
26
|
+
smtpHost: config.smtpHost.get().trim(),
|
|
27
|
+
smtpPort,
|
|
28
|
+
smtpSecure: smtpPort === 465,
|
|
52
29
|
};
|
|
30
|
+
}
|
|
31
|
+
export function apply(ctx, config) {
|
|
53
32
|
// sessionId -> agent handle
|
|
54
33
|
const handles = new Map();
|
|
55
|
-
// sessionId ->
|
|
34
|
+
// sessionId -> 最近一封来信(用于回复)
|
|
56
35
|
const lastMail = new Map();
|
|
57
36
|
// `${sessionId}:${turn}` -> 该 turn 内累积的 assistant 文本
|
|
58
37
|
const turnBuffers = new Map();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
38
|
+
let client;
|
|
39
|
+
let lock;
|
|
40
|
+
let startedKey = '';
|
|
41
|
+
let smtp;
|
|
42
|
+
let smtpKey = '';
|
|
43
|
+
const stopImap = async () => {
|
|
44
|
+
startedKey = '';
|
|
45
|
+
try {
|
|
46
|
+
lock?.release();
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
lock = undefined;
|
|
50
|
+
const current = client;
|
|
51
|
+
client = undefined;
|
|
52
|
+
if (current)
|
|
53
|
+
await current.logout().catch(() => { });
|
|
54
|
+
};
|
|
55
|
+
const ensureStarted = async () => {
|
|
56
|
+
const servers = resolveServers(config);
|
|
57
|
+
const key = JSON.stringify(servers);
|
|
58
|
+
if (key === startedKey)
|
|
59
|
+
return;
|
|
60
|
+
await stopImap();
|
|
61
|
+
if (!servers.email || !servers.password || !servers.imapHost || !servers.smtpHost) {
|
|
62
|
+
console.warn('[mail-bot] 请在 设置 → 插件 → dsh-mail-sender 填写邮箱、密码与 IMAP/SMTP 服务器');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const next = new ImapFlow({
|
|
66
|
+
host: servers.imapHost,
|
|
67
|
+
port: servers.imapPort,
|
|
68
|
+
secure: true,
|
|
69
|
+
auth: { user: servers.email, pass: servers.password },
|
|
70
|
+
logger: false,
|
|
71
|
+
});
|
|
72
|
+
client = next;
|
|
73
|
+
startedKey = key;
|
|
74
|
+
next.on('exists', async () => {
|
|
63
75
|
try {
|
|
64
|
-
const mb =
|
|
76
|
+
const mb = next.mailbox;
|
|
65
77
|
if (mb === false)
|
|
66
78
|
return;
|
|
67
|
-
const fetched = await
|
|
79
|
+
const fetched = await next.fetchOne(mb.exists, { source: true });
|
|
68
80
|
if (fetched === false || !fetched.source)
|
|
69
81
|
return;
|
|
70
82
|
const parsed = await simpleParser(fetched.source);
|
|
@@ -86,11 +98,6 @@ export function apply(ctx, config) {
|
|
|
86
98
|
if (!handle) {
|
|
87
99
|
handle = await getOrCreateAgent(ctx, sessionId);
|
|
88
100
|
handles.set(sessionId, handle);
|
|
89
|
-
ctx.on('session/event', (session, event) => {
|
|
90
|
-
if (session?.header?.id !== sessionId)
|
|
91
|
-
return;
|
|
92
|
-
handleSessionEvent(sessionId, event);
|
|
93
|
-
});
|
|
94
101
|
}
|
|
95
102
|
handle.agent.followup(createUserMessage({
|
|
96
103
|
content: [{ type: 'text', text: parsed.text || '' }],
|
|
@@ -101,18 +108,41 @@ export function apply(ctx, config) {
|
|
|
101
108
|
console.error('[mail-bot] 收信失败', err);
|
|
102
109
|
}
|
|
103
110
|
});
|
|
104
|
-
|
|
105
|
-
await
|
|
106
|
-
if (
|
|
107
|
-
return void
|
|
108
|
-
lock = await
|
|
109
|
-
}
|
|
111
|
+
try {
|
|
112
|
+
await next.connect();
|
|
113
|
+
if (client !== next)
|
|
114
|
+
return void next.logout().catch(() => { });
|
|
115
|
+
lock = await next.getMailboxLock('INBOX');
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error('[mail-bot] IMAP 连接失败', err);
|
|
119
|
+
if (client === next)
|
|
120
|
+
await stopImap();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const getSmtp = (servers) => {
|
|
124
|
+
const key = `${servers.smtpHost}:${servers.smtpPort}:${servers.smtpSecure}:${servers.email}`;
|
|
125
|
+
if (smtpKey !== key) {
|
|
126
|
+
smtp = nodemailer.createTransport({
|
|
127
|
+
host: servers.smtpHost,
|
|
128
|
+
port: servers.smtpPort,
|
|
129
|
+
secure: servers.smtpSecure,
|
|
130
|
+
requireTLS: !servers.smtpSecure,
|
|
131
|
+
auth: { user: servers.email, pass: servers.password },
|
|
132
|
+
});
|
|
133
|
+
smtpKey = key;
|
|
134
|
+
}
|
|
135
|
+
return smtp;
|
|
136
|
+
};
|
|
137
|
+
ctx.on('loader/volatile-update', () => { void ensureStarted(); });
|
|
138
|
+
ctx.on('session/event', (session, event) => {
|
|
139
|
+
const id = session?.header?.id;
|
|
140
|
+
if (typeof id === 'string' && handles.has(id))
|
|
141
|
+
handleSessionEvent(id, event);
|
|
142
|
+
});
|
|
143
|
+
ctx.effect(() => {
|
|
144
|
+
void ensureStarted();
|
|
110
145
|
return () => {
|
|
111
|
-
closed = true;
|
|
112
|
-
try {
|
|
113
|
-
lock?.release();
|
|
114
|
-
}
|
|
115
|
-
catch { }
|
|
116
146
|
for (const h of handles.values()) {
|
|
117
147
|
try {
|
|
118
148
|
h.dispose();
|
|
@@ -122,7 +152,7 @@ export function apply(ctx, config) {
|
|
|
122
152
|
handles.clear();
|
|
123
153
|
turnBuffers.clear();
|
|
124
154
|
lastMail.clear();
|
|
125
|
-
void
|
|
155
|
+
void stopImap();
|
|
126
156
|
};
|
|
127
157
|
});
|
|
128
158
|
function handleSessionEvent(sessionId, event) {
|
|
@@ -139,7 +169,7 @@ export function apply(ctx, config) {
|
|
|
139
169
|
turnBuffers.set(key, [...(turnBuffers.get(key) ?? []), text]);
|
|
140
170
|
return;
|
|
141
171
|
}
|
|
142
|
-
//
|
|
172
|
+
// 一轮完整结束后才回信;一个 turn 可能含多个 step
|
|
143
173
|
if (event.type === 'turn/end' && data.reason?.kind === 'completed') {
|
|
144
174
|
const replyText = (turnBuffers.get(key) ?? []).join('\n\n').trim();
|
|
145
175
|
turnBuffers.delete(key);
|
|
@@ -148,9 +178,10 @@ export function apply(ctx, config) {
|
|
|
148
178
|
const mail = lastMail.get(sessionId);
|
|
149
179
|
if (!mail)
|
|
150
180
|
return;
|
|
181
|
+
const servers = resolveServers(config);
|
|
151
182
|
const subject = mail.subject.startsWith('Re:') ? mail.subject : `Re: ${mail.subject}`;
|
|
152
|
-
getSmtp().sendMail({
|
|
153
|
-
from:
|
|
183
|
+
getSmtp(servers).sendMail({
|
|
184
|
+
from: servers.email,
|
|
154
185
|
to: mail.from,
|
|
155
186
|
subject,
|
|
156
187
|
text: replyText,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mail-sender",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "inbound IMAP mail is delivered to an agent, whose reply is sent back over SMTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -41,5 +41,10 @@
|
|
|
41
41
|
"@deepseek-ai/schemastery": "3.19.0",
|
|
42
42
|
"@types/nodemailer": "^6.4.17",
|
|
43
43
|
"typescript": "^5.6.0"
|
|
44
|
+
},
|
|
45
|
+
"pnpm": {
|
|
46
|
+
"overrides": {
|
|
47
|
+
"iconv-lite": "0.6.3"
|
|
48
|
+
}
|
|
44
49
|
}
|
|
45
50
|
}
|
package/readme.md
CHANGED
|
@@ -30,15 +30,7 @@ SMTP 回复 (Re: 原主题, In-Reply-To: 原 Message-ID)
|
|
|
30
30
|
## 安装
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
# npm
|
|
34
33
|
dsh plugin --profile web add dsh-mail-sender
|
|
35
|
-
|
|
36
|
-
# GitHub(需仓库内已提交 lib/)
|
|
37
|
-
dsh plugin --profile web add github:<你的用户名>/dsh-mail-sender
|
|
38
|
-
|
|
39
|
-
# 本地源码
|
|
40
|
-
npm install && npm run build
|
|
41
|
-
dsh plugin --profile web add link:/path/to/dsh-mail-sender
|
|
42
34
|
```
|
|
43
35
|
|
|
44
36
|
安装后重启 `dsh web`,用 `dsh --profile web --dump-config` 确认插件行已进入配置树。
|
|
@@ -53,21 +45,18 @@ dsh plugin --profile web add link:/path/to/dsh-mail-sender
|
|
|
53
45
|
|
|
54
46
|
| 字段 | 说明 |
|
|
55
47
|
|---|---|
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `smtp.host` | SMTP 服务器地址(如 `smtp.qq.com`) |
|
|
62
|
-
| `smtp.port` | 端口,默认 `465` |
|
|
63
|
-
| `smtp.secure` | 是否 TLS,默认 `true` |
|
|
64
|
-
| `smtp.user` | SMTP 登录账号(也是回信 From) |
|
|
65
|
-
| `smtp.password` | SMTP 密码或授权码 |
|
|
48
|
+
| `email` | 机器人邮箱,如 `agent@qq.com`(同时用于 IMAP/SMTP 登录与回信 From) |
|
|
49
|
+
| `password` | 邮箱密码或授权码 |
|
|
50
|
+
| `imapHost` | IMAP 服务器,如 `imap.qq.com`(固定 993 + SSL) |
|
|
51
|
+
| `smtpHost` | SMTP 服务器,如 `smtp.qq.com` |
|
|
52
|
+
| `smtpPort` | 默认 `465`(SSL);填 `587` 时自动走 STARTTLS |
|
|
66
53
|
| `allowedAddresses` | 允许与 agent 通信的邮箱地址列表 |
|
|
67
54
|
|
|
55
|
+
服务器地址按你的邮箱服务商填写,插件不做猜测。
|
|
56
|
+
|
|
68
57
|
白名单同时控制两个方向:非名单内发件人的来信直接丢弃;agent 的回复目标就是来信人,因此也天然限定在名单内。
|
|
69
58
|
|
|
70
|
-
|
|
59
|
+
所有字段都是 `.volatile()`:保存后即时生效,IMAP 会按新配置自动重连,无需重启 host。
|
|
71
60
|
|
|
72
61
|
## 包结构
|
|
73
62
|
|
|
@@ -86,33 +75,4 @@ dsh plugin --profile web add link:/path/to/dsh-mail-sender
|
|
|
86
75
|
- insert:
|
|
87
76
|
- id: dsh-mail-sender
|
|
88
77
|
name: dsh-mail-sender
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## 发布
|
|
92
|
-
|
|
93
|
-
### npm
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
npm run build # 产出 lib/
|
|
97
|
-
npm publish # scoped 包用 --access public
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`files` 已包含 `lib` 与 `cordis.patch.yml`——漏掉 patch 文件是最常见的“装上了但没进配置树”原因。
|
|
101
|
-
|
|
102
|
-
### GitHub
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
git init && git add . && git commit -m "feat: dsh-mail-sender"
|
|
106
|
-
git branch -M main
|
|
107
|
-
git remote add origin https://github.com/<用户名>/dsh-mail-sender.git
|
|
108
|
-
git push -u origin main
|
|
109
|
-
git tag v0.1.3 && git push origin v0.1.3
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
从 git 安装时 pnpm 需要执行构建,请在 profile 的 `pnpm-workspace.yaml` 里加 `allowBuilds: { dsh-mail-sender: true }`;更省事的方式是发布到 npm(发布产物已构建好)。
|
|
113
|
-
|
|
114
|
-
## 依赖
|
|
115
|
-
|
|
116
|
-
运行时:`imapflow`、`mailparser`、`nodemailer`。
|
|
117
|
-
|
|
118
|
-
Peer(由 DSH host 提供):`@deepseek-ai/cordis`、`@deepseek-ai/dsh-agent`、`@deepseek-ai/dsh-llm`、`@deepseek-ai/schemastery`。
|
|
78
|
+
```
|