@zhin.js/adapter-email 5.0.2 → 5.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @zhin.js/adapter-email
2
2
 
3
+ ## 5.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [c106ecc]
8
+ - Updated dependencies [b0f37ae]
9
+ - Updated dependencies [ba08a2f]
10
+ - Updated dependencies [daffd4c]
11
+ - Updated dependencies [36c7400]
12
+ - Updated dependencies [162fa34]
13
+ - Updated dependencies [e40b048]
14
+ - Updated dependencies [f1708c3]
15
+ - Updated dependencies [e53444f]
16
+ - Updated dependencies [92b0dd7]
17
+ - Updated dependencies [a7df753]
18
+ - @zhin.js/im-contract@1.0.3
19
+ - @zhin.js/adapter@1.1.7
20
+ - @zhin.js/plugin-runtime@1.1.5
21
+ - @zhin.js/core@1.5.4
22
+ - zhin.js@6.0.4
23
+
24
+ ## 5.0.3
25
+
26
+ ### Patch Changes
27
+
28
+ - f8c7a54: fix: im
29
+ - Updated dependencies [f8c7a54]
30
+ - @zhin.js/logger@1.0.76
31
+ - @zhin.js/adapter@1.1.6
32
+ - @zhin.js/core@1.5.3
33
+ - @zhin.js/im-contract@1.0.2
34
+ - @zhin.js/plugin-runtime@1.1.4
35
+ - zhin.js@6.0.3
36
+
3
37
  ## 5.0.2
4
38
 
5
39
  ### 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, getLogger } from '@zhin.js/logger';
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, name } = this.#options.config;
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({ endpoint: name, mode: 'smtp' }));
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({ endpoint: name, mode: 'imap' }));
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', endpoint: this.#options.config.name }));
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.name,
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.name,
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.name,
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 name?: string;
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 name: string;
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(endpointId: string, email: EmailMessage): ConversationRef;
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 name = (typeof config.name === 'string' && config.name)
16
- || (typeof entry?.name === 'string' && entry.name)
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
- name,
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(endpointId, email) {
127
+ export function emailInboundConversation(endpointKey, email) {
128
128
  return {
129
- endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
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.2",
3
+ "version": "5.0.4",
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.5",
13
- "@zhin.js/core": "1.5.2",
14
- "@zhin.js/im-contract": "1.0.1",
15
- "@zhin.js/logger": "1.0.75",
16
- "@zhin.js/plugin-runtime": "1.1.3"
12
+ "@zhin.js/adapter": "1.1.7",
13
+ "@zhin.js/core": "1.5.4",
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.5",
20
- "@zhin.js/core": "1.5.2",
21
- "@zhin.js/plugin-runtime": "1.1.3",
22
- "zhin.js": "6.0.2"
19
+ "@zhin.js/adapter": "1.1.7",
20
+ "@zhin.js/core": "1.5.4",
21
+ "@zhin.js/plugin-runtime": "1.1.5",
22
+ "zhin.js": "6.0.4"
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。每项与顶层字段同构(name 必填,其余覆盖顶层)",
26
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
9
27
  "items": {
10
28
  "type": "object",
11
29
  "additionalProperties": true,
12
30
  "properties": {
13
- "name": {
14
- "type": "string",
15
- "description": "Email bot name"
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
- "name",
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, getLogger } from '@zhin.js/logger';
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, name } = this.#options.config;
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({ endpoint: name, mode: 'smtp' }));
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({ endpoint: name, mode: 'imap' }));
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', endpoint: this.#options.config.name }));
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
- op: 'disconnect',
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.name,
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.name,
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.name,
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 name?: string;
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 name: string;
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 name = (typeof config.name === 'string' && config.name)
98
- || (typeof entry?.name === 'string' && entry.name)
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
- name,
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(endpointId: string, email: EmailMessage): ConversationRef {
235
+ export function emailInboundConversation(endpointKey: string, email: EmailMessage): ConversationRef {
236
236
  return {
237
- endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
237
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
238
238
  kind: 'private',
239
239
  id: email.from,
240
240
  };