@zhin.js/adapter-email 5.0.2 → 5.0.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/CHANGELOG.md +42 -0
- package/lib/endpoint.js +26 -26
- package/lib/protocol.d.ts +3 -3
- package/lib/protocol.js +5 -5
- package/package.json +10 -10
- package/schema.json +41 -5
- package/src/endpoint.ts +28 -28
- package/src/protocol.ts +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @zhin.js/adapter-email
|
|
2
2
|
|
|
3
|
+
## 5.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @zhin.js/adapter@1.1.7
|
|
8
|
+
- @zhin.js/core@1.5.5
|
|
9
|
+
- zhin.js@6.0.5
|
|
10
|
+
|
|
11
|
+
## 5.0.4
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [c106ecc]
|
|
16
|
+
- Updated dependencies [b0f37ae]
|
|
17
|
+
- Updated dependencies [ba08a2f]
|
|
18
|
+
- Updated dependencies [daffd4c]
|
|
19
|
+
- Updated dependencies [36c7400]
|
|
20
|
+
- Updated dependencies [162fa34]
|
|
21
|
+
- Updated dependencies [e40b048]
|
|
22
|
+
- Updated dependencies [f1708c3]
|
|
23
|
+
- Updated dependencies [e53444f]
|
|
24
|
+
- Updated dependencies [92b0dd7]
|
|
25
|
+
- Updated dependencies [a7df753]
|
|
26
|
+
- @zhin.js/im-contract@1.0.3
|
|
27
|
+
- @zhin.js/adapter@1.1.7
|
|
28
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
29
|
+
- @zhin.js/core@1.5.4
|
|
30
|
+
- zhin.js@6.0.4
|
|
31
|
+
|
|
32
|
+
## 5.0.3
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- f8c7a54: fix: im
|
|
37
|
+
- Updated dependencies [f8c7a54]
|
|
38
|
+
- @zhin.js/logger@1.0.76
|
|
39
|
+
- @zhin.js/adapter@1.1.6
|
|
40
|
+
- @zhin.js/core@1.5.3
|
|
41
|
+
- @zhin.js/im-contract@1.0.2
|
|
42
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
43
|
+
- zhin.js@6.0.3
|
|
44
|
+
|
|
3
45
|
## 5.0.2
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/lib/endpoint.js
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { simpleParser } from 'mailparser';
|
|
7
|
-
import { formatCompact,
|
|
7
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
8
|
import { emailInboundConversation, formatInboundContent, formatInboundSegments, formatOutboundMail, parseEmailMessage, senderDisplayName, } from './protocol.js';
|
|
9
9
|
import { defaultCreateImap, defaultCreateSmtp, } from './transport.js';
|
|
10
|
-
const logger = getLogger('email');
|
|
11
10
|
/**
|
|
12
11
|
* Email(SMTP/IMAP)无好友/群/频道等社交图谱概念,
|
|
13
12
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
14
13
|
*/
|
|
15
14
|
export class EmailEndpoint {
|
|
15
|
+
#logger;
|
|
16
16
|
#options;
|
|
17
17
|
#smtp = null;
|
|
18
18
|
#imap = null;
|
|
@@ -23,17 +23,18 @@ export class EmailEndpoint {
|
|
|
23
23
|
#open = false;
|
|
24
24
|
#started = false;
|
|
25
25
|
constructor(options) {
|
|
26
|
+
this.#logger = getAdapterLogger('email', options.config.id);
|
|
26
27
|
this.#options = options;
|
|
27
28
|
}
|
|
28
29
|
async start() {
|
|
29
30
|
if (this.#started)
|
|
30
31
|
return;
|
|
31
32
|
this.#started = true;
|
|
32
|
-
const { smtp, imap,
|
|
33
|
+
const { smtp, imap, id } = this.#options.config;
|
|
33
34
|
try {
|
|
34
35
|
this.#smtp = await (this.#options.createSmtp?.(smtp) ?? defaultCreateSmtp(smtp));
|
|
35
36
|
await this.#smtp.verify();
|
|
36
|
-
logger.debug(formatCompact({
|
|
37
|
+
this.#logger.debug(formatCompact({ mode: 'smtp' }));
|
|
37
38
|
this.#imap = this.#options.createImap?.(imap) ?? defaultCreateImap(imap);
|
|
38
39
|
this.#setupImapListeners(this.#imap);
|
|
39
40
|
await new Promise((resolve, reject) => {
|
|
@@ -41,13 +42,13 @@ export class EmailEndpoint {
|
|
|
41
42
|
this.#imap.once('error', (error) => reject(error));
|
|
42
43
|
this.#imap.connect();
|
|
43
44
|
});
|
|
44
|
-
logger.debug(formatCompact({
|
|
45
|
+
this.#logger.debug(formatCompact({ mode: 'imap' }));
|
|
45
46
|
this.#reconnectAttempts = 0;
|
|
46
47
|
this.#startEmailCheck();
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
49
50
|
await this.stop();
|
|
50
|
-
logger.error('Failed to connect email services:', error);
|
|
51
|
+
this.#logger.error('Failed to connect email services:', error);
|
|
51
52
|
throw error;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -87,7 +88,7 @@ export class EmailEndpoint {
|
|
|
87
88
|
}
|
|
88
89
|
this.#smtp = null;
|
|
89
90
|
}
|
|
90
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
91
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
91
92
|
}
|
|
92
93
|
async send({ conversation, payload }) {
|
|
93
94
|
if (!this.#smtp)
|
|
@@ -98,7 +99,7 @@ export class EmailEndpoint {
|
|
|
98
99
|
to: target,
|
|
99
100
|
});
|
|
100
101
|
const info = await this.#smtp.sendMail(mailOptions);
|
|
101
|
-
logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
|
|
102
|
+
this.#logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
|
|
102
103
|
return info.messageId || '';
|
|
103
104
|
}
|
|
104
105
|
/** Test / internal: admit a parsed mail when the endpoint is open. */
|
|
@@ -106,7 +107,7 @@ export class EmailEndpoint {
|
|
|
106
107
|
if (!this.#open)
|
|
107
108
|
return;
|
|
108
109
|
void this.#admitWithAttachments(email).catch((err) => {
|
|
109
|
-
logger.warn(formatCompact({
|
|
110
|
+
this.#logger.warn(formatCompact({
|
|
110
111
|
op: 'email_gateway_receive_failed',
|
|
111
112
|
target: email.from,
|
|
112
113
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -123,14 +124,14 @@ export class EmailEndpoint {
|
|
|
123
124
|
...(email.messageId ? { message: { conversation, id: email.messageId } } : {}),
|
|
124
125
|
content,
|
|
125
126
|
segments: formatInboundSegments(email, savedAttachments),
|
|
126
|
-
sender: senderDisplayName(sender),
|
|
127
|
+
sender: { id: sender, name: senderDisplayName(sender) || undefined },
|
|
128
|
+
endpointId: this.#options.config.id,
|
|
127
129
|
metadata: Object.freeze({
|
|
128
130
|
subject: email.subject,
|
|
129
131
|
to: email.to,
|
|
130
132
|
cc: email.cc,
|
|
131
133
|
uid: email.uid,
|
|
132
134
|
date: email.date.toISOString(),
|
|
133
|
-
endpoint: this.#options.config.name,
|
|
134
135
|
...(savedAttachments.length ? { attachments: savedAttachments } : {}),
|
|
135
136
|
}),
|
|
136
137
|
});
|
|
@@ -152,15 +153,15 @@ export class EmailEndpoint {
|
|
|
152
153
|
const filename = path.basename(rawName) || `attachment_${Date.now()}`;
|
|
153
154
|
const filepath = path.resolve(downloadRoot, filename);
|
|
154
155
|
if (filepath !== downloadRoot && !filepath.startsWith(downloadRoot + path.sep)) {
|
|
155
|
-
logger.warn(formatCompact({ op: 'email_attachment_skipped', filename: rawName, reason: 'path' }));
|
|
156
|
+
this.#logger.warn(formatCompact({ op: 'email_attachment_skipped', filename: rawName, reason: 'path' }));
|
|
156
157
|
continue;
|
|
157
158
|
}
|
|
158
159
|
if (config.allowedTypes?.length && !config.allowedTypes.includes(attachment.contentType ?? '')) {
|
|
159
|
-
logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'type' }));
|
|
160
|
+
this.#logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'type' }));
|
|
160
161
|
continue;
|
|
161
162
|
}
|
|
162
163
|
if (attachment.size != null && attachment.size > config.maxFileSize) {
|
|
163
|
-
logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'size' }));
|
|
164
|
+
this.#logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'size' }));
|
|
164
165
|
continue;
|
|
165
166
|
}
|
|
166
167
|
try {
|
|
@@ -168,7 +169,7 @@ export class EmailEndpoint {
|
|
|
168
169
|
saved.push({ filename, path: filepath, contentType: attachment.contentType, size: attachment.size });
|
|
169
170
|
}
|
|
170
171
|
catch (error) {
|
|
171
|
-
logger.warn(formatCompact({
|
|
172
|
+
this.#logger.warn(formatCompact({
|
|
172
173
|
op: 'email_attachment_download_failed',
|
|
173
174
|
filename,
|
|
174
175
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -182,14 +183,13 @@ export class EmailEndpoint {
|
|
|
182
183
|
void this.#checkForNewEmails();
|
|
183
184
|
});
|
|
184
185
|
imap.on('error', (error) => {
|
|
185
|
-
logger.error('IMAP error:', error);
|
|
186
|
+
this.#logger.error('IMAP error:', error);
|
|
186
187
|
// imap 通常在 error 后紧跟 end;两处都调度,靠已有定时器去重
|
|
187
188
|
this.#scheduleImapReconnect();
|
|
188
189
|
});
|
|
189
190
|
imap.on('end', () => {
|
|
190
|
-
logger.debug(formatCompact({
|
|
191
|
+
this.#logger.debug(formatCompact({
|
|
191
192
|
op: 'disconnect',
|
|
192
|
-
endpoint: this.#options.config.name,
|
|
193
193
|
mode: 'imap',
|
|
194
194
|
}));
|
|
195
195
|
this.#scheduleImapReconnect();
|
|
@@ -202,9 +202,9 @@ export class EmailEndpoint {
|
|
|
202
202
|
const base = this.#options.config.imap.reconnectInterval;
|
|
203
203
|
const delay = Math.min(base * 2 ** this.#reconnectAttempts, 300_000);
|
|
204
204
|
this.#reconnectAttempts += 1;
|
|
205
|
-
logger.warn(formatCompact({
|
|
205
|
+
this.#logger.warn(formatCompact({
|
|
206
206
|
op: 'imap_reconnect_scheduled',
|
|
207
|
-
endpoint: this.#options.config.
|
|
207
|
+
endpoint: this.#options.config.id,
|
|
208
208
|
reconnect_ms: delay,
|
|
209
209
|
}));
|
|
210
210
|
this.#reconnectTimer = setTimeout(() => {
|
|
@@ -226,17 +226,17 @@ export class EmailEndpoint {
|
|
|
226
226
|
imap.connect();
|
|
227
227
|
});
|
|
228
228
|
this.#reconnectAttempts = 0;
|
|
229
|
-
logger.info(formatCompact({
|
|
229
|
+
this.#logger.info(formatCompact({
|
|
230
230
|
op: 'imap_reconnect',
|
|
231
|
-
endpoint: this.#options.config.
|
|
231
|
+
endpoint: this.#options.config.id,
|
|
232
232
|
ok: true,
|
|
233
233
|
}));
|
|
234
234
|
void this.#checkForNewEmails();
|
|
235
235
|
}
|
|
236
236
|
catch (error) {
|
|
237
|
-
logger.warn(formatCompact({
|
|
237
|
+
this.#logger.warn(formatCompact({
|
|
238
238
|
op: 'imap_reconnect',
|
|
239
|
-
endpoint: this.#options.config.
|
|
239
|
+
endpoint: this.#options.config.id,
|
|
240
240
|
ok: false,
|
|
241
241
|
error: error instanceof Error ? error.message : String(error),
|
|
242
242
|
}));
|
|
@@ -280,7 +280,7 @@ export class EmailEndpoint {
|
|
|
280
280
|
});
|
|
281
281
|
}
|
|
282
282
|
catch (error) {
|
|
283
|
-
logger.error('Error checking for new emails:', error);
|
|
283
|
+
this.#logger.error('Error checking for new emails:', error);
|
|
284
284
|
}
|
|
285
285
|
finally {
|
|
286
286
|
this.#checking = false;
|
|
@@ -301,7 +301,7 @@ export class EmailEndpoint {
|
|
|
301
301
|
void simpleParser(body).then((parsed) => {
|
|
302
302
|
this.admit(parseEmailMessage(parsed, uid));
|
|
303
303
|
}).catch((error) => {
|
|
304
|
-
logger.error('Error parsing email:', error);
|
|
304
|
+
this.#logger.error('Error parsing email:', error);
|
|
305
305
|
});
|
|
306
306
|
});
|
|
307
307
|
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface EmailAttachmentsConfig {
|
|
|
34
34
|
}
|
|
35
35
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
36
36
|
export interface EmailAdapterConfig {
|
|
37
|
-
readonly
|
|
37
|
+
readonly id?: string;
|
|
38
38
|
readonly smtp?: SmtpConfig;
|
|
39
39
|
readonly imap?: ImapConfig;
|
|
40
40
|
readonly attachments?: EmailAttachmentsConfig;
|
|
@@ -45,7 +45,7 @@ export interface EmailAdapterConfig {
|
|
|
45
45
|
}
|
|
46
46
|
export interface ResolvedEmailConfig {
|
|
47
47
|
readonly context: 'email';
|
|
48
|
-
readonly
|
|
48
|
+
readonly id: string;
|
|
49
49
|
readonly smtp: SmtpConfig;
|
|
50
50
|
readonly imap: Required<Pick<ImapConfig, 'checkInterval' | 'reconnectInterval' | 'mailbox' | 'markSeen'>> & ImapConfig;
|
|
51
51
|
readonly attachments?: {
|
|
@@ -106,7 +106,7 @@ export declare function formatInboundSegments(email: EmailMessage, savedAttachme
|
|
|
106
106
|
* 入站归一化 → ConversationRef:Email 无群/频道概念,所有入站邮件都是
|
|
107
107
|
* 与发件人地址的 private 会话(id = 发件人地址)。
|
|
108
108
|
*/
|
|
109
|
-
export declare function emailInboundConversation(
|
|
109
|
+
export declare function emailInboundConversation(endpointKey: string, email: EmailMessage): ConversationRef;
|
|
110
110
|
export declare function senderDisplayName(from: string): string;
|
|
111
111
|
/**
|
|
112
112
|
* nodemailer 附件的最小形状:url/path 走 `path`(URL 由 nodemailer 拉流、
|
package/lib/protocol.js
CHANGED
|
@@ -12,8 +12,8 @@ export function resolveEmailConfig(config = {}) {
|
|
|
12
12
|
if (!smtp?.host || !smtp.auth?.user || !imap?.host || !imap.user) {
|
|
13
13
|
throw new TypeError('Email adapter requires smtp + imap config (plugins.<key>.smtp/imap or endpoints with context: email)');
|
|
14
14
|
}
|
|
15
|
-
const
|
|
16
|
-
|| (typeof entry?.
|
|
15
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
16
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
17
17
|
|| process.env.EMAIL_BOT_NAME
|
|
18
18
|
|| 'email-bot';
|
|
19
19
|
const attachmentsSource = config.attachments ?? entry?.attachments;
|
|
@@ -27,7 +27,7 @@ export function resolveEmailConfig(config = {}) {
|
|
|
27
27
|
: undefined;
|
|
28
28
|
return {
|
|
29
29
|
context: 'email',
|
|
30
|
-
|
|
30
|
+
id,
|
|
31
31
|
smtp,
|
|
32
32
|
imap: {
|
|
33
33
|
...imap,
|
|
@@ -124,9 +124,9 @@ export function formatInboundSegments(email, savedAttachments = []) {
|
|
|
124
124
|
* 入站归一化 → ConversationRef:Email 无群/频道概念,所有入站邮件都是
|
|
125
125
|
* 与发件人地址的 private 会话(id = 发件人地址)。
|
|
126
126
|
*/
|
|
127
|
-
export function emailInboundConversation(
|
|
127
|
+
export function emailInboundConversation(endpointKey, email) {
|
|
128
128
|
return {
|
|
129
|
-
endpoint: { id:
|
|
129
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
130
130
|
kind: 'private',
|
|
131
131
|
id: email.from,
|
|
132
132
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-email",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zhin.js Email adapter for Plugin Runtime (SMTP/IMAP)",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
"imap": "^0.8.19",
|
|
10
10
|
"mailparser": "^3.9.14",
|
|
11
11
|
"nodemailer": "^9.0.3",
|
|
12
|
-
"@zhin.js/adapter": "1.1.
|
|
13
|
-
"@zhin.js/core": "1.5.
|
|
14
|
-
"@zhin.js/im-contract": "1.0.
|
|
15
|
-
"@zhin.js/logger": "1.0.
|
|
16
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
12
|
+
"@zhin.js/adapter": "1.1.7",
|
|
13
|
+
"@zhin.js/core": "1.5.5",
|
|
14
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
15
|
+
"@zhin.js/logger": "1.0.76",
|
|
16
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"@zhin.js/adapter": "1.1.
|
|
20
|
-
"@zhin.js/core": "1.5.
|
|
21
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
22
|
-
"zhin.js": "6.0.
|
|
19
|
+
"@zhin.js/adapter": "1.1.7",
|
|
20
|
+
"@zhin.js/core": "1.5.5",
|
|
21
|
+
"@zhin.js/plugin-runtime": "1.1.5",
|
|
22
|
+
"zhin.js": "6.0.5"
|
|
23
23
|
},
|
|
24
24
|
"peerDependenciesMeta": {
|
|
25
25
|
"zhin.js": {
|
package/schema.json
CHANGED
|
@@ -3,16 +3,48 @@
|
|
|
3
3
|
"type": "object",
|
|
4
4
|
"additionalProperties": false,
|
|
5
5
|
"properties": {
|
|
6
|
+
"master": {
|
|
7
|
+
"type": [
|
|
8
|
+
"string",
|
|
9
|
+
"number"
|
|
10
|
+
],
|
|
11
|
+
"description": "框架 master(email address;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
|
|
12
|
+
},
|
|
13
|
+
"trusted": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": [
|
|
17
|
+
"string",
|
|
18
|
+
"number"
|
|
19
|
+
],
|
|
20
|
+
"description": "Trusted email address"
|
|
21
|
+
},
|
|
22
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
23
|
+
},
|
|
6
24
|
"endpoints": {
|
|
7
25
|
"type": "array",
|
|
8
|
-
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(
|
|
26
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
9
27
|
"items": {
|
|
10
28
|
"type": "object",
|
|
11
29
|
"additionalProperties": true,
|
|
12
30
|
"properties": {
|
|
13
|
-
"
|
|
14
|
-
"type":
|
|
15
|
-
|
|
31
|
+
"master": {
|
|
32
|
+
"type": [
|
|
33
|
+
"string",
|
|
34
|
+
"number"
|
|
35
|
+
],
|
|
36
|
+
"description": "本 endpoint 的框架 master(email address);覆盖顶层 master"
|
|
37
|
+
},
|
|
38
|
+
"trusted": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": [
|
|
42
|
+
"string",
|
|
43
|
+
"number"
|
|
44
|
+
],
|
|
45
|
+
"description": "Trusted email address"
|
|
46
|
+
},
|
|
47
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
16
48
|
},
|
|
17
49
|
"smtp": {
|
|
18
50
|
"type": "object",
|
|
@@ -112,10 +144,14 @@
|
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
}
|
|
147
|
+
},
|
|
148
|
+
"id": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "Email bot name"
|
|
115
151
|
}
|
|
116
152
|
},
|
|
117
153
|
"required": [
|
|
118
|
-
"
|
|
154
|
+
"id",
|
|
119
155
|
"smtp",
|
|
120
156
|
"imap"
|
|
121
157
|
]
|
package/src/endpoint.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as path from 'node:path';
|
|
|
6
6
|
import { simpleParser } from 'mailparser';
|
|
7
7
|
import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
|
|
8
8
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
9
|
-
import { formatCompact,
|
|
9
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
10
10
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
11
11
|
import {
|
|
12
12
|
emailInboundConversation,
|
|
@@ -27,8 +27,6 @@ import {
|
|
|
27
27
|
type EmailSmtpTransport,
|
|
28
28
|
} from './transport.js';
|
|
29
29
|
|
|
30
|
-
const logger = getLogger('email');
|
|
31
|
-
|
|
32
30
|
export interface EmailEndpointOptions {
|
|
33
31
|
readonly id: CapabilityId;
|
|
34
32
|
readonly gateway: MessageGateway;
|
|
@@ -42,6 +40,8 @@ export interface EmailEndpointOptions {
|
|
|
42
40
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
43
41
|
*/
|
|
44
42
|
export class EmailEndpoint implements EndpointInstance {
|
|
43
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
44
|
+
|
|
45
45
|
readonly #options: EmailEndpointOptions;
|
|
46
46
|
#smtp: EmailSmtpTransport | null = null;
|
|
47
47
|
#imap: EmailImapTransport | null = null;
|
|
@@ -53,17 +53,18 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
53
53
|
#started = false;
|
|
54
54
|
|
|
55
55
|
constructor(options: EmailEndpointOptions) {
|
|
56
|
+
this.#logger = getAdapterLogger('email', options.config.id);
|
|
56
57
|
this.#options = options;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async start(): Promise<void> {
|
|
60
61
|
if (this.#started) return;
|
|
61
62
|
this.#started = true;
|
|
62
|
-
const { smtp, imap,
|
|
63
|
+
const { smtp, imap, id } = this.#options.config;
|
|
63
64
|
try {
|
|
64
65
|
this.#smtp = await (this.#options.createSmtp?.(smtp) ?? defaultCreateSmtp(smtp));
|
|
65
66
|
await this.#smtp.verify();
|
|
66
|
-
logger.debug(formatCompact({
|
|
67
|
+
this.#logger.debug(formatCompact({ mode: 'smtp' }));
|
|
67
68
|
|
|
68
69
|
this.#imap = this.#options.createImap?.(imap) ?? defaultCreateImap(imap);
|
|
69
70
|
this.#setupImapListeners(this.#imap);
|
|
@@ -72,12 +73,12 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
72
73
|
this.#imap!.once('error', (error) => reject(error));
|
|
73
74
|
this.#imap!.connect();
|
|
74
75
|
});
|
|
75
|
-
logger.debug(formatCompact({
|
|
76
|
+
this.#logger.debug(formatCompact({ mode: 'imap' }));
|
|
76
77
|
this.#reconnectAttempts = 0;
|
|
77
78
|
this.#startEmailCheck();
|
|
78
79
|
} catch (error) {
|
|
79
80
|
await this.stop();
|
|
80
|
-
logger.error('Failed to connect email services:', error);
|
|
81
|
+
this.#logger.error('Failed to connect email services:', error);
|
|
81
82
|
throw error;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -118,7 +119,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
118
119
|
}
|
|
119
120
|
this.#smtp = null;
|
|
120
121
|
}
|
|
121
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
122
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
@@ -129,7 +130,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
129
130
|
to: target,
|
|
130
131
|
});
|
|
131
132
|
const info = await this.#smtp.sendMail(mailOptions);
|
|
132
|
-
logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
|
|
133
|
+
this.#logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
|
|
133
134
|
return info.messageId || '';
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -137,7 +138,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
137
138
|
admit(email: EmailMessage): void {
|
|
138
139
|
if (!this.#open) return;
|
|
139
140
|
void this.#admitWithAttachments(email).catch((err) => {
|
|
140
|
-
logger.warn(formatCompact({
|
|
141
|
+
this.#logger.warn(formatCompact({
|
|
141
142
|
op: 'email_gateway_receive_failed',
|
|
142
143
|
target: email.from,
|
|
143
144
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -155,14 +156,14 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
155
156
|
...(email.messageId ? { message: { conversation, id: email.messageId } } : {}),
|
|
156
157
|
content,
|
|
157
158
|
segments: formatInboundSegments(email, savedAttachments),
|
|
158
|
-
sender: senderDisplayName(sender),
|
|
159
|
+
sender: { id: sender, name: senderDisplayName(sender) || undefined },
|
|
160
|
+
endpointId: this.#options.config.id,
|
|
159
161
|
metadata: Object.freeze({
|
|
160
162
|
subject: email.subject,
|
|
161
163
|
to: email.to,
|
|
162
164
|
cc: email.cc,
|
|
163
165
|
uid: email.uid,
|
|
164
166
|
date: email.date.toISOString(),
|
|
165
|
-
endpoint: this.#options.config.name,
|
|
166
167
|
...(savedAttachments.length ? { attachments: savedAttachments } : {}),
|
|
167
168
|
}),
|
|
168
169
|
});
|
|
@@ -186,22 +187,22 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
186
187
|
const filename = path.basename(rawName) || `attachment_${Date.now()}`;
|
|
187
188
|
const filepath = path.resolve(downloadRoot, filename);
|
|
188
189
|
if (filepath !== downloadRoot && !filepath.startsWith(downloadRoot + path.sep)) {
|
|
189
|
-
logger.warn(formatCompact({ op: 'email_attachment_skipped', filename: rawName, reason: 'path' }));
|
|
190
|
+
this.#logger.warn(formatCompact({ op: 'email_attachment_skipped', filename: rawName, reason: 'path' }));
|
|
190
191
|
continue;
|
|
191
192
|
}
|
|
192
193
|
if (config.allowedTypes?.length && !config.allowedTypes.includes(attachment.contentType ?? '')) {
|
|
193
|
-
logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'type' }));
|
|
194
|
+
this.#logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'type' }));
|
|
194
195
|
continue;
|
|
195
196
|
}
|
|
196
197
|
if (attachment.size != null && attachment.size > config.maxFileSize) {
|
|
197
|
-
logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'size' }));
|
|
198
|
+
this.#logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'size' }));
|
|
198
199
|
continue;
|
|
199
200
|
}
|
|
200
201
|
try {
|
|
201
202
|
await writeFile(filepath, attachment.content);
|
|
202
203
|
saved.push({ filename, path: filepath, contentType: attachment.contentType, size: attachment.size });
|
|
203
204
|
} catch (error) {
|
|
204
|
-
logger.warn(formatCompact({
|
|
205
|
+
this.#logger.warn(formatCompact({
|
|
205
206
|
op: 'email_attachment_download_failed',
|
|
206
207
|
filename,
|
|
207
208
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -216,14 +217,13 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
216
217
|
void this.#checkForNewEmails();
|
|
217
218
|
});
|
|
218
219
|
imap.on('error', (error) => {
|
|
219
|
-
logger.error('IMAP error:', error);
|
|
220
|
+
this.#logger.error('IMAP error:', error);
|
|
220
221
|
// imap 通常在 error 后紧跟 end;两处都调度,靠已有定时器去重
|
|
221
222
|
this.#scheduleImapReconnect();
|
|
222
223
|
});
|
|
223
224
|
imap.on('end', () => {
|
|
224
|
-
logger.debug(formatCompact({
|
|
225
|
-
|
|
226
|
-
endpoint: this.#options.config.name,
|
|
225
|
+
this.#logger.debug(formatCompact({
|
|
226
|
+
op: 'disconnect',
|
|
227
227
|
mode: 'imap',
|
|
228
228
|
}));
|
|
229
229
|
this.#scheduleImapReconnect();
|
|
@@ -236,9 +236,9 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
236
236
|
const base = this.#options.config.imap.reconnectInterval;
|
|
237
237
|
const delay = Math.min(base * 2 ** this.#reconnectAttempts, 300_000);
|
|
238
238
|
this.#reconnectAttempts += 1;
|
|
239
|
-
logger.warn(formatCompact({
|
|
239
|
+
this.#logger.warn(formatCompact({
|
|
240
240
|
op: 'imap_reconnect_scheduled',
|
|
241
|
-
endpoint: this.#options.config.
|
|
241
|
+
endpoint: this.#options.config.id,
|
|
242
242
|
reconnect_ms: delay,
|
|
243
243
|
}));
|
|
244
244
|
this.#reconnectTimer = setTimeout(() => {
|
|
@@ -260,16 +260,16 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
260
260
|
imap.connect();
|
|
261
261
|
});
|
|
262
262
|
this.#reconnectAttempts = 0;
|
|
263
|
-
logger.info(formatCompact({
|
|
263
|
+
this.#logger.info(formatCompact({
|
|
264
264
|
op: 'imap_reconnect',
|
|
265
|
-
endpoint: this.#options.config.
|
|
265
|
+
endpoint: this.#options.config.id,
|
|
266
266
|
ok: true,
|
|
267
267
|
}));
|
|
268
268
|
void this.#checkForNewEmails();
|
|
269
269
|
} catch (error) {
|
|
270
|
-
logger.warn(formatCompact({
|
|
270
|
+
this.#logger.warn(formatCompact({
|
|
271
271
|
op: 'imap_reconnect',
|
|
272
|
-
endpoint: this.#options.config.
|
|
272
|
+
endpoint: this.#options.config.id,
|
|
273
273
|
ok: false,
|
|
274
274
|
error: error instanceof Error ? error.message : String(error),
|
|
275
275
|
}));
|
|
@@ -309,7 +309,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
309
309
|
});
|
|
310
310
|
});
|
|
311
311
|
} catch (error) {
|
|
312
|
-
logger.error('Error checking for new emails:', error);
|
|
312
|
+
this.#logger.error('Error checking for new emails:', error);
|
|
313
313
|
} finally {
|
|
314
314
|
this.#checking = false;
|
|
315
315
|
}
|
|
@@ -330,7 +330,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
330
330
|
void simpleParser(body).then((parsed) => {
|
|
331
331
|
this.admit(parseEmailMessage(parsed, uid));
|
|
332
332
|
}).catch((error) => {
|
|
333
|
-
logger.error('Error parsing email:', error);
|
|
333
|
+
this.#logger.error('Error parsing email:', error);
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
336
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface EmailAttachmentsConfig {
|
|
|
43
43
|
|
|
44
44
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
45
45
|
export interface EmailAdapterConfig {
|
|
46
|
-
readonly
|
|
46
|
+
readonly id?: string;
|
|
47
47
|
readonly smtp?: SmtpConfig;
|
|
48
48
|
readonly imap?: ImapConfig;
|
|
49
49
|
readonly attachments?: EmailAttachmentsConfig;
|
|
@@ -55,7 +55,7 @@ export interface EmailAdapterConfig {
|
|
|
55
55
|
|
|
56
56
|
export interface ResolvedEmailConfig {
|
|
57
57
|
readonly context: 'email';
|
|
58
|
-
readonly
|
|
58
|
+
readonly id: string;
|
|
59
59
|
readonly smtp: SmtpConfig;
|
|
60
60
|
readonly imap: Required<Pick<ImapConfig, 'checkInterval' | 'reconnectInterval' | 'mailbox' | 'markSeen'>> & ImapConfig;
|
|
61
61
|
readonly attachments?: {
|
|
@@ -94,8 +94,8 @@ export function resolveEmailConfig(config: EmailAdapterConfig = {}): ResolvedEma
|
|
|
94
94
|
'Email adapter requires smtp + imap config (plugins.<key>.smtp/imap or endpoints with context: email)',
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
|
-
const
|
|
98
|
-
|| (typeof entry?.
|
|
97
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
98
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
99
99
|
|| process.env.EMAIL_BOT_NAME
|
|
100
100
|
|| 'email-bot';
|
|
101
101
|
const attachmentsSource = config.attachments ?? entry?.attachments;
|
|
@@ -109,7 +109,7 @@ export function resolveEmailConfig(config: EmailAdapterConfig = {}): ResolvedEma
|
|
|
109
109
|
: undefined;
|
|
110
110
|
return {
|
|
111
111
|
context: 'email',
|
|
112
|
-
|
|
112
|
+
id,
|
|
113
113
|
smtp,
|
|
114
114
|
imap: {
|
|
115
115
|
...imap,
|
|
@@ -232,9 +232,9 @@ export function formatInboundSegments(
|
|
|
232
232
|
* 入站归一化 → ConversationRef:Email 无群/频道概念,所有入站邮件都是
|
|
233
233
|
* 与发件人地址的 private 会话(id = 发件人地址)。
|
|
234
234
|
*/
|
|
235
|
-
export function emailInboundConversation(
|
|
235
|
+
export function emailInboundConversation(endpointKey: string, email: EmailMessage): ConversationRef {
|
|
236
236
|
return {
|
|
237
|
-
endpoint: { id:
|
|
237
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
238
238
|
kind: 'private',
|
|
239
239
|
id: email.from,
|
|
240
240
|
};
|