dsh-mail-sender 0.1.3 → 0.1.4

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 CHANGED
@@ -7,78 +7,30 @@ interface Ref<T> {
7
7
  get(): T;
8
8
  }
9
9
  export interface Config {
10
- imap: {
11
- host: Ref<string>;
12
- port: Ref<number>;
13
- secure: Ref<boolean>;
14
- user: Ref<string>;
15
- password: Ref<string>;
16
- };
17
- smtp: {
18
- host: Ref<string>;
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
- imap: Schema<Schemastery.ObjectS<NoInfer<{
29
- host: Schema<string, string, "volatile-defined">;
30
- port: Schema<number, number, "volatile-defined">;
31
- secure: Schema<boolean, boolean, "volatile-defined">;
32
- user: Schema<string, string, "volatile-defined">;
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
- imap: Schema<Schemastery.ObjectS<NoInfer<{
57
- host: Schema<string, string, "volatile-defined">;
58
- port: Schema<number, number, "volatile-defined">;
59
- secure: Schema<boolean, boolean, "volatile-defined">;
60
- user: Schema<string, string, "volatile-defined">;
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
- imap: Schema.object({
10
- host: Schema.string().volatile().default('').description('IMAP 服务器地址'),
11
- port: Schema.number().volatile().default(993),
12
- secure: Schema.boolean().volatile().default(true),
13
- user: Schema.string().volatile().default('').description('IMAP 登录账号'),
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
- export function apply(ctx, config) {
29
- const imapConfig = () => ({
30
- host: config.imap.host.get(),
31
- port: config.imap.port.get(),
32
- secure: config.imap.secure.get(),
33
- auth: { user: config.imap.user.get(), pass: config.imap.password.get() },
34
- });
35
- const imap = new ImapFlow({ ...imapConfig(), logger: false });
36
- // SMTP 传输按当前配置缓存,设置变更后自动重建
37
- let smtp;
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
- ctx.effect(() => {
60
- let closed = false;
61
- let lock;
62
- imap.on('exists', async () => {
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 = imap.mailbox;
76
+ const mb = next.mailbox;
65
77
  if (mb === false)
66
78
  return;
67
- const fetched = await imap.fetchOne(mb.exists, { source: true });
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
- void (async () => {
105
- await imap.connect();
106
- if (closed)
107
- return void imap.logout();
108
- lock = await imap.getMailboxLock('INBOX');
109
- })().catch(err => console.error('[mail-bot] IMAP 连接失败', err));
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 imap.logout().catch(() => { });
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
- // 一轮完整结束后才回复;一个 turn 可能含多个 step
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: config.smtp.user.get(),
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.3",
4
- "description": "DSH mail channel: inbound IMAP mail is delivered to an agent, whose reply is sent back over SMTP.",
3
+ "version": "0.1.4",
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",
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
- | `imap.host` | IMAP 服务器地址(如 `imap.qq.com`) |
57
- | `imap.port` | 端口,默认 `993` |
58
- | `imap.secure` | 是否 TLS,默认 `true` |
59
- | `imap.user` | IMAP 登录账号 |
60
- | `imap.password` | IMAP 密码或授权码 |
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
- 配置为 `.volatile()`:白名单与 SMTP 设置在保存后即时生效;IMAP 连接参数变更后需重载插件(重启 `dsh web`)才重连。
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
+ ```