@zhin.js/adapter-email 6.0.0 → 6.0.2
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 +26 -0
- package/README.md +1 -1
- package/adapters/email.js +0 -3
- package/adapters/email.ts +0 -3
- package/lib/client.d.ts +23 -0
- package/lib/client.js +29 -0
- package/lib/endpoint.d.ts +5 -5
- package/lib/endpoint.js +44 -17
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +1 -1
- package/package.json +9 -8
- package/src/client.ts +40 -0
- package/src/endpoint.ts +46 -20
- package/src/index.ts +6 -0
- package/src/protocol.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @zhin.js/adapter-email
|
|
2
2
|
|
|
3
|
+
## 6.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ba7e17a]
|
|
8
|
+
- Updated dependencies [7108d0b]
|
|
9
|
+
- @zhin.js/core@1.5.15
|
|
10
|
+
- zhin.js@6.0.15
|
|
11
|
+
|
|
12
|
+
## 6.0.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- e9c6a73: Ignore late disconnect events from replaced IMAP transports so reconnect does not leak duplicate connections.
|
|
17
|
+
- 1fc78bc: Unify native platform Client access behind the literal `adapter` discriminant. Handlers infer both native events and Clients, while command, inbound/outbound middleware, and both Agent tool authoring surfaces expose the exact operation-scoped Client through a lazy `$client` getter. Definitions without `adapter` keep `$client` typed as `unknown`, and runtime dispatch rejects adapter mismatches before resolving the Client. Bundled platform tools now use this single path instead of model-provided endpoint ids and adapter-specific dependency wrappers. Every adapter registers one Client/EventMap contract, and protocol adapters including NapCat, Milky, OneBot and Satori now produce transport-independent Client objects rather than letting Endpoint instances impersonate Clients.
|
|
18
|
+
- Updated dependencies [4e8117c]
|
|
19
|
+
- Updated dependencies [54bfd6b]
|
|
20
|
+
- Updated dependencies [12025ee]
|
|
21
|
+
- Updated dependencies [09b14d6]
|
|
22
|
+
- Updated dependencies [1fc78bc]
|
|
23
|
+
- @zhin.js/adapter@1.2.1
|
|
24
|
+
- @zhin.js/core@1.5.14
|
|
25
|
+
- @zhin.js/logger@1.0.77
|
|
26
|
+
- zhin.js@6.0.14
|
|
27
|
+
- @zhin.js/feature-kit@1.0.13
|
|
28
|
+
|
|
3
29
|
## 6.0.0
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ pnpm add @zhin.js/adapter-email
|
|
|
19
19
|
## Plugin Runtime
|
|
20
20
|
|
|
21
21
|
- `@zhin.js/adapter` — 约定式 `adapters/email.ts`(`defineAdapter`)
|
|
22
|
-
- `@zhin.js/core` — `
|
|
22
|
+
- `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
|
|
23
23
|
- `zhin.js` — `plugin.ts`(`definePlugin`)
|
|
24
24
|
- 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`(`smtp` / `imap`)
|
|
25
25
|
|
package/adapters/email.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/email.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
6
|
import { EmailEndpoint } from "../lib/endpoint.js";
|
|
8
7
|
import { resolveEmailConfig, } from "../lib/protocol.js";
|
|
9
8
|
export { EmailEndpoint } from "../lib/endpoint.js";
|
|
@@ -20,8 +19,6 @@ export default defineAdapter({
|
|
|
20
19
|
create(context) {
|
|
21
20
|
return new EmailEndpoint({
|
|
22
21
|
id: context.id,
|
|
23
|
-
gateway: context.use(messageGatewayToken),
|
|
24
|
-
sideEvents: context.use(sideEventGatewayToken),
|
|
25
22
|
config: resolveEmailConfig(context.config),
|
|
26
23
|
});
|
|
27
24
|
},
|
package/adapters/email.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/email.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
5
|
import { EmailEndpoint } from '../src/endpoint.js';
|
|
7
6
|
import {
|
|
8
7
|
resolveEmailConfig,
|
|
@@ -30,8 +29,6 @@ export default defineAdapter<EmailAdapterConfig>({
|
|
|
30
29
|
create(context) {
|
|
31
30
|
return new EmailEndpoint({
|
|
32
31
|
id: context.id,
|
|
33
|
-
gateway: context.use(messageGatewayToken),
|
|
34
|
-
sideEvents: context.use(sideEventGatewayToken),
|
|
35
32
|
config: resolveEmailConfig(context.config),
|
|
36
33
|
});
|
|
37
34
|
},
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EmailImapTransport, EmailSmtpTransport } from './transport.js';
|
|
2
|
+
/** Live SMTP + IMAP client pair exposed to event handlers and plugins. */
|
|
3
|
+
export declare class EmailClient {
|
|
4
|
+
private readonly resolveSmtp;
|
|
5
|
+
private readonly resolveImap;
|
|
6
|
+
constructor(resolveSmtp: () => EmailSmtpTransport | null, resolveImap: () => EmailImapTransport | null);
|
|
7
|
+
get smtp(): EmailSmtpTransport;
|
|
8
|
+
get imap(): EmailImapTransport;
|
|
9
|
+
verify(): Promise<void>;
|
|
10
|
+
sendMail(options: unknown): Promise<{
|
|
11
|
+
messageId?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export type EmailClientEventMap = Record<string, unknown>;
|
|
15
|
+
declare module '@zhin.js/feature-kit' {
|
|
16
|
+
interface AdapterClientRegistry {
|
|
17
|
+
readonly email: {
|
|
18
|
+
readonly client: EmailClient;
|
|
19
|
+
readonly events: EmailClientEventMap;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export declare const emailClient: import("@zhin.js/adapter").EndpointClientToken<EmailClient, EmailClientEventMap>;
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
2
|
+
/** Live SMTP + IMAP client pair exposed to event handlers and plugins. */
|
|
3
|
+
export class EmailClient {
|
|
4
|
+
resolveSmtp;
|
|
5
|
+
resolveImap;
|
|
6
|
+
constructor(resolveSmtp, resolveImap) {
|
|
7
|
+
this.resolveSmtp = resolveSmtp;
|
|
8
|
+
this.resolveImap = resolveImap;
|
|
9
|
+
}
|
|
10
|
+
get smtp() {
|
|
11
|
+
const transport = this.resolveSmtp();
|
|
12
|
+
if (!transport)
|
|
13
|
+
throw new Error('SMTP transporter not connected');
|
|
14
|
+
return transport;
|
|
15
|
+
}
|
|
16
|
+
get imap() {
|
|
17
|
+
const transport = this.resolveImap();
|
|
18
|
+
if (!transport)
|
|
19
|
+
throw new Error('IMAP client not connected');
|
|
20
|
+
return transport;
|
|
21
|
+
}
|
|
22
|
+
verify() {
|
|
23
|
+
return this.smtp.verify();
|
|
24
|
+
}
|
|
25
|
+
sendMail(options) {
|
|
26
|
+
return this.smtp.sendMail(options);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const emailClient = defineEndpointClient('email');
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
2
|
+
import { type EndpointSendRequest } from 'zhin.js/adapter';
|
|
3
3
|
import type { CapabilityId } from 'zhin.js';
|
|
4
4
|
import { type EmailMessage, type ResolvedEmailConfig } from './protocol.js';
|
|
5
5
|
import { type EmailImapTransport, type EmailSmtpTransport } from './transport.js';
|
|
6
|
+
import { EmailClient } from './client.js';
|
|
6
7
|
export interface EmailEndpointOptions {
|
|
7
8
|
readonly id: CapabilityId;
|
|
8
|
-
readonly gateway: MessageGateway;
|
|
9
|
-
readonly sideEvents?: SideEventGateway;
|
|
10
9
|
readonly config: ResolvedEmailConfig;
|
|
11
10
|
readonly createSmtp?: (config: ResolvedEmailConfig['smtp']) => EmailSmtpTransport | Promise<EmailSmtpTransport>;
|
|
12
11
|
readonly createImap?: (config: ResolvedEmailConfig['imap']) => EmailImapTransport;
|
|
@@ -15,8 +14,9 @@ export interface EmailEndpointOptions {
|
|
|
15
14
|
* Email(SMTP/IMAP)无好友/群/频道等社交图谱概念,
|
|
16
15
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
17
16
|
*/
|
|
18
|
-
export declare class EmailEndpoint
|
|
17
|
+
export declare class EmailEndpoint extends Endpoint<EmailClient> {
|
|
19
18
|
#private;
|
|
19
|
+
readonly client: EmailClient;
|
|
20
20
|
constructor(options: EmailEndpointOptions);
|
|
21
21
|
start(): Promise<void>;
|
|
22
22
|
open(): void;
|
package/lib/endpoint.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
1
2
|
/**
|
|
2
3
|
* EmailEndpoint — lifecycle, SMTP outbound, IMAP inbound polling.
|
|
3
4
|
*/
|
|
@@ -7,11 +8,13 @@ import { simpleParser } from 'mailparser';
|
|
|
7
8
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
9
|
import { emailInboundConversation, formatInboundContent, formatInboundSegments, formatOutboundMail, parseEmailMessage, senderDisplayName, } from './protocol.js';
|
|
9
10
|
import { defaultCreateImap, defaultCreateSmtp, } from './transport.js';
|
|
11
|
+
import { EmailClient } from './client.js';
|
|
10
12
|
/**
|
|
11
13
|
* Email(SMTP/IMAP)无好友/群/频道等社交图谱概念,
|
|
12
14
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
13
15
|
*/
|
|
14
|
-
export class EmailEndpoint {
|
|
16
|
+
export class EmailEndpoint extends Endpoint {
|
|
17
|
+
client;
|
|
15
18
|
#logger;
|
|
16
19
|
#options;
|
|
17
20
|
#smtp = null;
|
|
@@ -23,8 +26,10 @@ export class EmailEndpoint {
|
|
|
23
26
|
#open = false;
|
|
24
27
|
#started = false;
|
|
25
28
|
constructor(options) {
|
|
29
|
+
super();
|
|
26
30
|
this.#logger = getAdapterLogger('email', options.config.id);
|
|
27
31
|
this.#options = options;
|
|
32
|
+
this.client = new EmailClient(() => this.#smtp, () => this.#imap);
|
|
28
33
|
}
|
|
29
34
|
async start() {
|
|
30
35
|
if (this.#started)
|
|
@@ -38,7 +43,10 @@ export class EmailEndpoint {
|
|
|
38
43
|
this.#imap = this.#options.createImap?.(imap) ?? defaultCreateImap(imap);
|
|
39
44
|
this.#setupImapListeners(this.#imap);
|
|
40
45
|
await new Promise((resolve, reject) => {
|
|
41
|
-
this.#imap.once('ready', () =>
|
|
46
|
+
this.#imap.once('ready', () => {
|
|
47
|
+
void this.#emitPlatformEvent('imap.ready', Object.freeze({}));
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
42
50
|
this.#imap.once('error', (error) => reject(error));
|
|
43
51
|
this.#imap.connect();
|
|
44
52
|
});
|
|
@@ -91,14 +99,12 @@ export class EmailEndpoint {
|
|
|
91
99
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
92
100
|
}
|
|
93
101
|
async send({ conversation, payload }) {
|
|
94
|
-
if (!this.#smtp)
|
|
95
|
-
throw new Error('SMTP transporter not initialized');
|
|
96
102
|
const target = conversation.id;
|
|
97
103
|
const mailOptions = formatOutboundMail(payload, {
|
|
98
104
|
from: this.#options.config.smtp.auth.user,
|
|
99
105
|
to: target,
|
|
100
106
|
});
|
|
101
|
-
const info = await this
|
|
107
|
+
const info = await this.client.sendMail(mailOptions);
|
|
102
108
|
this.#logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
|
|
103
109
|
return info.messageId || '';
|
|
104
110
|
}
|
|
@@ -106,6 +112,7 @@ export class EmailEndpoint {
|
|
|
106
112
|
admit(email) {
|
|
107
113
|
if (!this.#open)
|
|
108
114
|
return;
|
|
115
|
+
void this.#emitPlatformEvent('mail', email);
|
|
109
116
|
void this.#admitWithAttachments(email).catch((err) => {
|
|
110
117
|
this.#logger.warn(formatCompact({
|
|
111
118
|
op: 'email_gateway_receive_failed',
|
|
@@ -119,7 +126,7 @@ export class EmailEndpoint {
|
|
|
119
126
|
const content = formatInboundContent(email);
|
|
120
127
|
const sender = email.from;
|
|
121
128
|
const conversation = emailInboundConversation(String(this.#options.id), email);
|
|
122
|
-
await this
|
|
129
|
+
await this.emit('message.receive', {
|
|
123
130
|
conversation,
|
|
124
131
|
...(email.messageId ? { message: { conversation, id: email.messageId } } : {}),
|
|
125
132
|
content,
|
|
@@ -180,24 +187,39 @@ export class EmailEndpoint {
|
|
|
180
187
|
}
|
|
181
188
|
#setupImapListeners(imap) {
|
|
182
189
|
imap.on('mail', () => {
|
|
190
|
+
void this.#emitPlatformEvent('imap.mail', Object.freeze({}));
|
|
183
191
|
void this.#checkForNewEmails();
|
|
184
192
|
});
|
|
185
193
|
imap.on('error', (error) => {
|
|
194
|
+
void this.#emitPlatformEvent('imap.error', error);
|
|
186
195
|
this.#logger.error('IMAP error:', error);
|
|
187
196
|
// imap 通常在 error 后紧跟 end;两处都调度,靠已有定时器去重
|
|
188
|
-
this.#scheduleImapReconnect();
|
|
197
|
+
this.#scheduleImapReconnect(imap);
|
|
189
198
|
});
|
|
190
199
|
imap.on('end', () => {
|
|
200
|
+
void this.#emitPlatformEvent('imap.end', Object.freeze({}));
|
|
191
201
|
this.#logger.debug(formatCompact({
|
|
192
202
|
op: 'disconnect',
|
|
193
203
|
mode: 'imap',
|
|
194
204
|
}));
|
|
195
|
-
this.#scheduleImapReconnect();
|
|
205
|
+
this.#scheduleImapReconnect(imap);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async #emitPlatformEvent(name, event) {
|
|
209
|
+
await this.emitPlatform(name, event).catch((error) => {
|
|
210
|
+
this.#logger.warn(formatCompact({
|
|
211
|
+
op: 'email_platform_event_failed',
|
|
212
|
+
event: name,
|
|
213
|
+
error: error instanceof Error ? error.message : String(error),
|
|
214
|
+
}));
|
|
196
215
|
});
|
|
197
216
|
}
|
|
198
217
|
/** IMAP 断线后按指数退避重建连接并恢复监听(基数 reconnectInterval,封顶 5 分钟)。 */
|
|
199
|
-
#scheduleImapReconnect() {
|
|
200
|
-
|
|
218
|
+
#scheduleImapReconnect(source) {
|
|
219
|
+
// A replaced transport may emit a late `end` after its earlier `error`
|
|
220
|
+
// already caused a successful reconnect. Only the currently owned IMAP
|
|
221
|
+
// connection may arm the next generation's reconnect timer.
|
|
222
|
+
if (!this.#started || this.#reconnectTimer || (source && this.#imap !== source))
|
|
201
223
|
return;
|
|
202
224
|
const base = this.#options.config.imap.reconnectInterval;
|
|
203
225
|
const delay = Math.min(base * 2 ** this.#reconnectAttempts, 300_000);
|
|
@@ -215,15 +237,20 @@ export class EmailEndpoint {
|
|
|
215
237
|
async #reconnectImap() {
|
|
216
238
|
if (!this.#started)
|
|
217
239
|
return;
|
|
240
|
+
let imap;
|
|
218
241
|
try {
|
|
219
|
-
const
|
|
242
|
+
const nextImap = this.#options.createImap?.(this.#options.config.imap)
|
|
220
243
|
?? defaultCreateImap(this.#options.config.imap);
|
|
221
|
-
|
|
222
|
-
this.#
|
|
244
|
+
imap = nextImap;
|
|
245
|
+
this.#imap = nextImap;
|
|
246
|
+
this.#setupImapListeners(nextImap);
|
|
223
247
|
await new Promise((resolve, reject) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
248
|
+
nextImap.once('ready', () => {
|
|
249
|
+
void this.#emitPlatformEvent('imap.ready', Object.freeze({ reconnect: true }));
|
|
250
|
+
resolve();
|
|
251
|
+
});
|
|
252
|
+
nextImap.once('error', (error) => reject(error));
|
|
253
|
+
nextImap.connect();
|
|
227
254
|
});
|
|
228
255
|
this.#reconnectAttempts = 0;
|
|
229
256
|
this.#logger.info(formatCompact({
|
|
@@ -240,7 +267,7 @@ export class EmailEndpoint {
|
|
|
240
267
|
ok: false,
|
|
241
268
|
error: error instanceof Error ? error.message : String(error),
|
|
242
269
|
}));
|
|
243
|
-
this.#scheduleImapReconnect();
|
|
270
|
+
this.#scheduleImapReconnect(imap);
|
|
244
271
|
}
|
|
245
272
|
}
|
|
246
273
|
#startEmailCheck() {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { addressListText, emailInboundConversation, formatInboundContent, formatInboundSegments, formatOutboundMail, htmlToText, parseEmailMessage, resolveEmailConfig, senderDisplayName, type EmailAdapterConfig, type EmailAttachmentsConfig, type EmailMessage, type EmailWireSegment, type ImapConfig, type ResolvedEmailConfig, type SavedEmailAttachment, type SmtpConfig, } from './protocol.js';
|
|
2
|
+
export { EmailClient, emailClient, type EmailClientEventMap, } from './client.js';
|
|
2
3
|
export { EmailEndpoint, type EmailEndpointOptions, } from './endpoint.js';
|
|
3
4
|
export { defaultCreateImap, defaultCreateSmtp, type EmailImapFetchMessage, type EmailImapTransport, type EmailSmtpTransport, } from './transport.js';
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { addressListText, emailInboundConversation, formatInboundContent, formatInboundSegments, formatOutboundMail, htmlToText, parseEmailMessage, resolveEmailConfig, senderDisplayName, } from './protocol.js';
|
|
2
|
+
export { EmailClient, emailClient, } from './client.js';
|
|
2
3
|
export { EmailEndpoint, } from './endpoint.js';
|
|
3
4
|
export { defaultCreateImap, defaultCreateSmtp, } from './transport.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export declare function parseEmailMessage(parsed: {
|
|
|
87
87
|
attachments?: Attachment[];
|
|
88
88
|
date?: Date;
|
|
89
89
|
}, uid: number): EmailMessage;
|
|
90
|
-
/** Build inbound text for
|
|
90
|
+
/** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
|
|
91
91
|
export declare function formatInboundContent(email: EmailMessage): string;
|
|
92
92
|
/** 已落盘的入站附件(attachments.enabled 下载结果)。 */
|
|
93
93
|
export interface SavedEmailAttachment {
|
package/lib/protocol.js
CHANGED
|
@@ -72,7 +72,7 @@ export function parseEmailMessage(parsed, uid) {
|
|
|
72
72
|
uid,
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
/** Build inbound text for
|
|
75
|
+
/** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
|
|
76
76
|
export function formatInboundContent(email) {
|
|
77
77
|
const parts = [];
|
|
78
78
|
if (email.subject)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-email",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zhin.js Email adapter for Plugin Runtime (SMTP/IMAP)",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
"imap": "^0.8.19",
|
|
10
10
|
"mailparser": "^3.9.14",
|
|
11
11
|
"nodemailer": "^9.0.3",
|
|
12
|
-
"@zhin.js/adapter": "1.2.
|
|
13
|
-
"@zhin.js/core": "1.5.
|
|
12
|
+
"@zhin.js/adapter": "1.2.1",
|
|
13
|
+
"@zhin.js/core": "1.5.15",
|
|
14
|
+
"@zhin.js/feature-kit": "1.0.13",
|
|
14
15
|
"@zhin.js/im-contract": "1.0.4",
|
|
15
|
-
"@zhin.js/logger": "1.0.
|
|
16
|
+
"@zhin.js/logger": "1.0.77"
|
|
16
17
|
},
|
|
17
18
|
"peerDependencies": {
|
|
18
|
-
"@zhin.js/adapter": "1.2.
|
|
19
|
-
"@zhin.js/core": "1.5.
|
|
20
|
-
"zhin.js": "6.0.
|
|
19
|
+
"@zhin.js/adapter": "1.2.1",
|
|
20
|
+
"@zhin.js/core": "1.5.15",
|
|
21
|
+
"zhin.js": "6.0.15"
|
|
21
22
|
},
|
|
22
23
|
"peerDependenciesMeta": {
|
|
23
24
|
"zhin.js": {
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"@types/nodemailer": "^8.0.1",
|
|
32
33
|
"typescript": "^6.0.3",
|
|
33
34
|
"vitest": "^4.1.10",
|
|
34
|
-
"zhin.js": "6.0.
|
|
35
|
+
"zhin.js": "6.0.15"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
37
38
|
"zhin",
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EmailImapTransport, EmailSmtpTransport } from './transport.js';
|
|
2
|
+
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
3
|
+
|
|
4
|
+
/** Live SMTP + IMAP client pair exposed to event handlers and plugins. */
|
|
5
|
+
export class EmailClient {
|
|
6
|
+
constructor(
|
|
7
|
+
private readonly resolveSmtp: () => EmailSmtpTransport | null,
|
|
8
|
+
private readonly resolveImap: () => EmailImapTransport | null,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
get smtp(): EmailSmtpTransport {
|
|
12
|
+
const transport = this.resolveSmtp();
|
|
13
|
+
if (!transport) throw new Error('SMTP transporter not connected');
|
|
14
|
+
return transport;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get imap(): EmailImapTransport {
|
|
18
|
+
const transport = this.resolveImap();
|
|
19
|
+
if (!transport) throw new Error('IMAP client not connected');
|
|
20
|
+
return transport;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
verify(): Promise<void> {
|
|
24
|
+
return this.smtp.verify();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
sendMail(options: unknown): Promise<{ messageId?: string }> {
|
|
28
|
+
return this.smtp.sendMail(options);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type EmailClientEventMap = Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
declare module '@zhin.js/feature-kit' {
|
|
35
|
+
interface AdapterClientRegistry {
|
|
36
|
+
readonly email: { readonly client: EmailClient; readonly events: EmailClientEventMap };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const emailClient = defineEndpointClient<EmailClient, EmailClientEventMap>('email');
|
package/src/endpoint.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
1
2
|
/**
|
|
2
3
|
* EmailEndpoint — lifecycle, SMTP outbound, IMAP inbound polling.
|
|
3
4
|
*/
|
|
4
5
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
6
|
import * as path from 'node:path';
|
|
6
7
|
import { simpleParser } from 'mailparser';
|
|
7
|
-
import
|
|
8
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
8
|
+
import { type EndpointSendRequest } from 'zhin.js/adapter';
|
|
9
9
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
10
10
|
import type { CapabilityId } from 'zhin.js';
|
|
11
11
|
import {
|
|
@@ -26,11 +26,10 @@ import {
|
|
|
26
26
|
type EmailImapTransport,
|
|
27
27
|
type EmailSmtpTransport,
|
|
28
28
|
} from './transport.js';
|
|
29
|
+
import { EmailClient } from './client.js';
|
|
29
30
|
|
|
30
31
|
export interface EmailEndpointOptions {
|
|
31
32
|
readonly id: CapabilityId;
|
|
32
|
-
readonly gateway: MessageGateway;
|
|
33
|
-
readonly sideEvents?: SideEventGateway;
|
|
34
33
|
readonly config: ResolvedEmailConfig;
|
|
35
34
|
readonly createSmtp?: (config: ResolvedEmailConfig['smtp']) => EmailSmtpTransport | Promise<EmailSmtpTransport>;
|
|
36
35
|
readonly createImap?: (config: ResolvedEmailConfig['imap']) => EmailImapTransport;
|
|
@@ -40,7 +39,8 @@ export interface EmailEndpointOptions {
|
|
|
40
39
|
* Email(SMTP/IMAP)无好友/群/频道等社交图谱概念,
|
|
41
40
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
42
41
|
*/
|
|
43
|
-
export class EmailEndpoint
|
|
42
|
+
export class EmailEndpoint extends Endpoint<EmailClient> {
|
|
43
|
+
readonly client: EmailClient;
|
|
44
44
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
45
45
|
|
|
46
46
|
readonly #options: EmailEndpointOptions;
|
|
@@ -54,8 +54,10 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
54
54
|
#started = false;
|
|
55
55
|
|
|
56
56
|
constructor(options: EmailEndpointOptions) {
|
|
57
|
+
super();
|
|
57
58
|
this.#logger = getAdapterLogger('email', options.config.id);
|
|
58
59
|
this.#options = options;
|
|
60
|
+
this.client = new EmailClient(() => this.#smtp, () => this.#imap);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
async start(): Promise<void> {
|
|
@@ -70,7 +72,10 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
70
72
|
this.#imap = this.#options.createImap?.(imap) ?? defaultCreateImap(imap);
|
|
71
73
|
this.#setupImapListeners(this.#imap);
|
|
72
74
|
await new Promise<void>((resolve, reject) => {
|
|
73
|
-
this.#imap!.once('ready', () =>
|
|
75
|
+
this.#imap!.once('ready', () => {
|
|
76
|
+
void this.#emitPlatformEvent('imap.ready', Object.freeze({}));
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
74
79
|
this.#imap!.once('error', (error) => reject(error));
|
|
75
80
|
this.#imap!.connect();
|
|
76
81
|
});
|
|
@@ -124,13 +129,12 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
127
|
-
if (!this.#smtp) throw new Error('SMTP transporter not initialized');
|
|
128
132
|
const target = conversation.id;
|
|
129
133
|
const mailOptions = formatOutboundMail(payload, {
|
|
130
134
|
from: this.#options.config.smtp.auth.user,
|
|
131
135
|
to: target,
|
|
132
136
|
});
|
|
133
|
-
const info = await this
|
|
137
|
+
const info = await this.client.sendMail(mailOptions);
|
|
134
138
|
this.#logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
|
|
135
139
|
return info.messageId || '';
|
|
136
140
|
}
|
|
@@ -138,6 +142,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
138
142
|
/** Test / internal: admit a parsed mail when the endpoint is open. */
|
|
139
143
|
admit(email: EmailMessage): void {
|
|
140
144
|
if (!this.#open) return;
|
|
145
|
+
void this.#emitPlatformEvent('mail', email);
|
|
141
146
|
void this.#admitWithAttachments(email).catch((err) => {
|
|
142
147
|
this.#logger.warn(formatCompact({
|
|
143
148
|
op: 'email_gateway_receive_failed',
|
|
@@ -152,7 +157,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
152
157
|
const content = formatInboundContent(email);
|
|
153
158
|
const sender = email.from;
|
|
154
159
|
const conversation = emailInboundConversation(String(this.#options.id), email);
|
|
155
|
-
await this
|
|
160
|
+
await this.emit('message.receive', {
|
|
156
161
|
conversation,
|
|
157
162
|
...(email.messageId ? { message: { conversation, id: email.messageId } } : {}),
|
|
158
163
|
content,
|
|
@@ -215,25 +220,41 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
215
220
|
|
|
216
221
|
#setupImapListeners(imap: EmailImapTransport): void {
|
|
217
222
|
imap.on('mail', () => {
|
|
223
|
+
void this.#emitPlatformEvent('imap.mail', Object.freeze({}));
|
|
218
224
|
void this.#checkForNewEmails();
|
|
219
225
|
});
|
|
220
226
|
imap.on('error', (error) => {
|
|
227
|
+
void this.#emitPlatformEvent('imap.error', error);
|
|
221
228
|
this.#logger.error('IMAP error:', error);
|
|
222
229
|
// imap 通常在 error 后紧跟 end;两处都调度,靠已有定时器去重
|
|
223
|
-
this.#scheduleImapReconnect();
|
|
230
|
+
this.#scheduleImapReconnect(imap);
|
|
224
231
|
});
|
|
225
232
|
imap.on('end', () => {
|
|
233
|
+
void this.#emitPlatformEvent('imap.end', Object.freeze({}));
|
|
226
234
|
this.#logger.debug(formatCompact({
|
|
227
235
|
op: 'disconnect',
|
|
228
236
|
mode: 'imap',
|
|
229
237
|
}));
|
|
230
|
-
this.#scheduleImapReconnect();
|
|
238
|
+
this.#scheduleImapReconnect(imap);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async #emitPlatformEvent(name: string, event: unknown): Promise<void> {
|
|
243
|
+
await this.emitPlatform(name, event).catch((error) => {
|
|
244
|
+
this.#logger.warn(formatCompact({
|
|
245
|
+
op: 'email_platform_event_failed',
|
|
246
|
+
event: name,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
248
|
+
}));
|
|
231
249
|
});
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
/** IMAP 断线后按指数退避重建连接并恢复监听(基数 reconnectInterval,封顶 5 分钟)。 */
|
|
235
|
-
#scheduleImapReconnect(): void {
|
|
236
|
-
|
|
253
|
+
#scheduleImapReconnect(source?: EmailImapTransport): void {
|
|
254
|
+
// A replaced transport may emit a late `end` after its earlier `error`
|
|
255
|
+
// already caused a successful reconnect. Only the currently owned IMAP
|
|
256
|
+
// connection may arm the next generation's reconnect timer.
|
|
257
|
+
if (!this.#started || this.#reconnectTimer || (source && this.#imap !== source)) return;
|
|
237
258
|
const base = this.#options.config.imap.reconnectInterval;
|
|
238
259
|
const delay = Math.min(base * 2 ** this.#reconnectAttempts, 300_000);
|
|
239
260
|
this.#reconnectAttempts += 1;
|
|
@@ -250,15 +271,20 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
250
271
|
|
|
251
272
|
async #reconnectImap(): Promise<void> {
|
|
252
273
|
if (!this.#started) return;
|
|
274
|
+
let imap: EmailImapTransport | undefined;
|
|
253
275
|
try {
|
|
254
|
-
const
|
|
276
|
+
const nextImap = this.#options.createImap?.(this.#options.config.imap)
|
|
255
277
|
?? defaultCreateImap(this.#options.config.imap);
|
|
256
|
-
|
|
257
|
-
this.#
|
|
278
|
+
imap = nextImap;
|
|
279
|
+
this.#imap = nextImap;
|
|
280
|
+
this.#setupImapListeners(nextImap);
|
|
258
281
|
await new Promise<void>((resolve, reject) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
nextImap.once('ready', () => {
|
|
283
|
+
void this.#emitPlatformEvent('imap.ready', Object.freeze({ reconnect: true }));
|
|
284
|
+
resolve();
|
|
285
|
+
});
|
|
286
|
+
nextImap.once('error', (error) => reject(error));
|
|
287
|
+
nextImap.connect();
|
|
262
288
|
});
|
|
263
289
|
this.#reconnectAttempts = 0;
|
|
264
290
|
this.#logger.info(formatCompact({
|
|
@@ -274,7 +300,7 @@ export class EmailEndpoint implements EndpointInstance {
|
|
|
274
300
|
ok: false,
|
|
275
301
|
error: error instanceof Error ? error.message : String(error),
|
|
276
302
|
}));
|
|
277
|
-
this.#scheduleImapReconnect();
|
|
303
|
+
this.#scheduleImapReconnect(imap);
|
|
278
304
|
}
|
|
279
305
|
}
|
|
280
306
|
|
package/src/index.ts
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -171,7 +171,7 @@ export function parseEmailMessage(
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/** Build inbound text for
|
|
174
|
+
/** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
|
|
175
175
|
export function formatInboundContent(email: EmailMessage): string {
|
|
176
176
|
const parts: string[] = [];
|
|
177
177
|
if (email.subject) parts.push(`Subject: ${email.subject}`, '');
|