@zhin.js/adapter-email 1.0.0 → 1.1.0
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 +383 -0
- package/README.md +50 -102
- package/adapters/email.js +25 -0
- package/adapters/email.ts +35 -0
- package/lib/client.d.ts +23 -0
- package/lib/client.js +29 -0
- package/lib/endpoint.d.ts +28 -0
- package/lib/endpoint.js +335 -0
- package/lib/index.d.ts +4 -10
- package/lib/index.js +4 -23
- package/lib/protocol.d.ts +138 -0
- package/lib/protocol.js +245 -0
- package/lib/transport.d.ts +32 -0
- package/lib/transport.js +27 -0
- package/package.json +45 -12
- package/plugin.js +8 -0
- package/schema.json +169 -0
- package/src/client.ts +40 -0
- package/src/endpoint.ts +364 -0
- package/src/index.ts +35 -28
- package/src/protocol.ts +384 -0
- package/src/transport.ts +65 -0
- package/lib/adapter.d.ts +0 -11
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -14
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -33
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -368
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts +0 -49
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/src/adapter.ts +0 -16
- package/src/bot.ts +0 -420
- package/src/types.ts +0 -52
- /package/{skills/email/SKILL.md → agent/skills/email.md} +0 -0
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email SMTP/IMAP helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
import { htmlToPlainTextWithBlockBreaks, isMediaRef } from '@zhin.js/core';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
const logger = getLogger('email');
|
|
8
|
+
export function resolveEmailConfig(config = {}) {
|
|
9
|
+
const entry = config.endpoints?.find((item) => item.context === 'email');
|
|
10
|
+
const smtp = config.smtp ?? entry?.smtp;
|
|
11
|
+
const imap = config.imap ?? entry?.imap;
|
|
12
|
+
if (!smtp?.host || !smtp.auth?.user || !imap?.host || !imap.user) {
|
|
13
|
+
throw new TypeError('Email adapter requires smtp + imap config (plugins.<key>.smtp/imap or endpoints with context: email)');
|
|
14
|
+
}
|
|
15
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
16
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
17
|
+
|| process.env.EMAIL_BOT_NAME
|
|
18
|
+
|| 'email-bot';
|
|
19
|
+
const attachmentsSource = config.attachments ?? entry?.attachments;
|
|
20
|
+
const attachments = attachmentsSource?.enabled
|
|
21
|
+
? {
|
|
22
|
+
enabled: true,
|
|
23
|
+
downloadPath: attachmentsSource.downloadPath || './downloads/email',
|
|
24
|
+
maxFileSize: Math.max(attachmentsSource.maxFileSize || 10 * 1024 * 1024, 1),
|
|
25
|
+
allowedTypes: attachmentsSource.allowedTypes,
|
|
26
|
+
}
|
|
27
|
+
: undefined;
|
|
28
|
+
return {
|
|
29
|
+
context: 'email',
|
|
30
|
+
id,
|
|
31
|
+
smtp,
|
|
32
|
+
imap: {
|
|
33
|
+
...imap,
|
|
34
|
+
// 数值下限:0/负数会导致 setInterval(0) 风暴
|
|
35
|
+
checkInterval: Math.max(imap.checkInterval ?? 60_000, 1_000),
|
|
36
|
+
reconnectInterval: Math.max(imap.reconnectInterval ?? 5_000, 1_000),
|
|
37
|
+
mailbox: imap.mailbox ?? 'INBOX',
|
|
38
|
+
markSeen: imap.markSeen !== false,
|
|
39
|
+
},
|
|
40
|
+
attachments,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function htmlToText(html) {
|
|
44
|
+
return htmlToPlainTextWithBlockBreaks(html);
|
|
45
|
+
}
|
|
46
|
+
export function addressListText(addr) {
|
|
47
|
+
if (!addr)
|
|
48
|
+
return [];
|
|
49
|
+
if (Array.isArray(addr)) {
|
|
50
|
+
return addr.map((item) => addressText(item));
|
|
51
|
+
}
|
|
52
|
+
return [addressText(addr)];
|
|
53
|
+
}
|
|
54
|
+
function addressText(addr) {
|
|
55
|
+
if (!addr || typeof addr !== 'object')
|
|
56
|
+
return String(addr ?? '');
|
|
57
|
+
const record = addr;
|
|
58
|
+
return record.text || record.address || String(addr);
|
|
59
|
+
}
|
|
60
|
+
export function parseEmailMessage(parsed, uid) {
|
|
61
|
+
return {
|
|
62
|
+
messageId: parsed.messageId || '',
|
|
63
|
+
from: parsed.from ? addressListText(parsed.from)[0] || '' : '',
|
|
64
|
+
to: addressListText(parsed.to),
|
|
65
|
+
cc: addressListText(parsed.cc),
|
|
66
|
+
bcc: addressListText(parsed.bcc),
|
|
67
|
+
subject: parsed.subject || '',
|
|
68
|
+
text: parsed.text || '',
|
|
69
|
+
html: parsed.html ? String(parsed.html) : '',
|
|
70
|
+
attachments: parsed.attachments || [],
|
|
71
|
+
date: parsed.date || new Date(),
|
|
72
|
+
uid,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
|
|
76
|
+
export function formatInboundContent(email) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (email.subject)
|
|
79
|
+
parts.push(`Subject: ${email.subject}`, '');
|
|
80
|
+
if (email.text) {
|
|
81
|
+
parts.push(email.text);
|
|
82
|
+
}
|
|
83
|
+
else if (email.html) {
|
|
84
|
+
const fromHtml = htmlToText(email.html);
|
|
85
|
+
if (fromHtml)
|
|
86
|
+
parts.push(fromHtml);
|
|
87
|
+
}
|
|
88
|
+
for (const attachment of email.attachments) {
|
|
89
|
+
const kind = attachment.contentType?.startsWith('image/') ? 'image' : 'file';
|
|
90
|
+
const name = attachment.filename || 'attachment';
|
|
91
|
+
parts.push(`[${kind}: ${name}]`);
|
|
92
|
+
}
|
|
93
|
+
const text = parts.join('\n').trim();
|
|
94
|
+
return text || '(Empty email)';
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 入站邮件 → canonical Segment[](与 formatInboundContent 纯文本视图同源双轨)。
|
|
98
|
+
* 已落盘附件映射为 image/file 段,MediaRef kind=path 指向下载路径;
|
|
99
|
+
* 未下载的附件(disabled / 被过滤)只保留 content 里的占位文本。
|
|
100
|
+
*/
|
|
101
|
+
export function formatInboundSegments(email, savedAttachments = []) {
|
|
102
|
+
const out = [];
|
|
103
|
+
const content = formatInboundContent(email);
|
|
104
|
+
if (content)
|
|
105
|
+
out.push({ type: 'text', data: { text: content } });
|
|
106
|
+
for (const saved of savedAttachments) {
|
|
107
|
+
const type = saved.contentType?.startsWith('image/') ? 'image' : 'file';
|
|
108
|
+
out.push({
|
|
109
|
+
type,
|
|
110
|
+
data: {
|
|
111
|
+
media: {
|
|
112
|
+
kind: 'path',
|
|
113
|
+
value: saved.path,
|
|
114
|
+
...(saved.contentType ? { mime_type: saved.contentType } : {}),
|
|
115
|
+
},
|
|
116
|
+
name: saved.filename,
|
|
117
|
+
...(type === 'image' ? { alt: saved.filename } : {}),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 入站归一化 → ConversationRef:Email 无群/频道概念,所有入站邮件都是
|
|
125
|
+
* 与发件人地址的 private 会话(id = 发件人地址)。
|
|
126
|
+
*/
|
|
127
|
+
export function emailInboundConversation(endpointKey, email) {
|
|
128
|
+
return {
|
|
129
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
130
|
+
kind: 'private',
|
|
131
|
+
id: email.from,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function senderDisplayName(from) {
|
|
135
|
+
const name = from.split('<')[0]?.trim();
|
|
136
|
+
return name || from;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* image/audio/video/file 段 → nodemailer 附件(canonical MediaRef 唯一来源):
|
|
140
|
+
* - kind=url / path → attachment.path;
|
|
141
|
+
* - kind=base64 → attachment.content(data: URL 前缀剥离);
|
|
142
|
+
* - kind=file(平台不透明引用)邮件无对应概念,丢弃留痕;
|
|
143
|
+
* - 缺 media 同样 warn + 丢弃。
|
|
144
|
+
*/
|
|
145
|
+
function mediaSegmentToAttachment(type, data) {
|
|
146
|
+
const media = data.media;
|
|
147
|
+
if (!isMediaRef(media)) {
|
|
148
|
+
logger.warn(formatCompact({
|
|
149
|
+
op: 'email_outbound_media_dropped',
|
|
150
|
+
type,
|
|
151
|
+
reason: 'missing_media_ref',
|
|
152
|
+
}));
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (media.kind === 'file') {
|
|
156
|
+
logger.warn(formatCompact({
|
|
157
|
+
op: 'email_outbound_media_dropped',
|
|
158
|
+
type,
|
|
159
|
+
reason: 'unsupported_kind',
|
|
160
|
+
kind: media.kind,
|
|
161
|
+
}));
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const filename = attachmentFileName(type, data, media);
|
|
165
|
+
if (media.kind === 'base64') {
|
|
166
|
+
const value = media.value.startsWith('data:')
|
|
167
|
+
? media.value.slice(media.value.indexOf(',') + 1)
|
|
168
|
+
: media.value;
|
|
169
|
+
return { filename, content: value, encoding: 'base64' };
|
|
170
|
+
}
|
|
171
|
+
return { filename, path: media.value };
|
|
172
|
+
}
|
|
173
|
+
function attachmentFileName(type, data, media) {
|
|
174
|
+
if (media.file_name)
|
|
175
|
+
return media.file_name;
|
|
176
|
+
if (typeof data.name === 'string' && data.name)
|
|
177
|
+
return data.name;
|
|
178
|
+
if (typeof data.alt === 'string' && data.alt)
|
|
179
|
+
return data.alt;
|
|
180
|
+
return type === 'image' ? 'image.png' : 'file';
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Wire-encode an already-rendered outbound payload into nodemailer options.
|
|
184
|
+
* Segment canonicalization is intentionally not done here.
|
|
185
|
+
*/
|
|
186
|
+
export function formatOutboundMail(payload, options) {
|
|
187
|
+
const mail = {
|
|
188
|
+
from: options.from,
|
|
189
|
+
to: options.to,
|
|
190
|
+
subject: options.subject ?? 'Message from Bot',
|
|
191
|
+
};
|
|
192
|
+
if (typeof payload === 'string') {
|
|
193
|
+
return { ...mail, text: payload };
|
|
194
|
+
}
|
|
195
|
+
const segments = Array.isArray(payload)
|
|
196
|
+
? payload
|
|
197
|
+
: payload && typeof payload === 'object' && 'type' in payload
|
|
198
|
+
? [payload]
|
|
199
|
+
: [];
|
|
200
|
+
if (segments.length === 0) {
|
|
201
|
+
return {
|
|
202
|
+
...mail,
|
|
203
|
+
text: payload == null ? '' : typeof payload === 'object'
|
|
204
|
+
? JSON.stringify(payload)
|
|
205
|
+
: String(payload),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const textParts = [];
|
|
209
|
+
const htmlParts = [];
|
|
210
|
+
const attachments = [];
|
|
211
|
+
for (const item of segments) {
|
|
212
|
+
if (typeof item === 'string') {
|
|
213
|
+
textParts.push(item);
|
|
214
|
+
htmlParts.push(item.replace(/\n/g, '<br>'));
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const data = item.data ?? {};
|
|
218
|
+
switch (item.type) {
|
|
219
|
+
case 'text': {
|
|
220
|
+
const textContent = String(data.text ?? data.content ?? '');
|
|
221
|
+
textParts.push(textContent);
|
|
222
|
+
htmlParts.push(textContent.replace(/\n/g, '<br>'));
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'image':
|
|
226
|
+
case 'audio':
|
|
227
|
+
case 'video':
|
|
228
|
+
case 'file': {
|
|
229
|
+
const attachment = mediaSegmentToAttachment(item.type, data);
|
|
230
|
+
if (attachment)
|
|
231
|
+
attachments.push(attachment);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
default:
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
...mail,
|
|
240
|
+
...(textParts.length > 0
|
|
241
|
+
? { text: textParts.join('\n'), html: htmlParts.join('<br>') }
|
|
242
|
+
: {}),
|
|
243
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ResolvedEmailConfig } from './protocol.js';
|
|
2
|
+
export interface EmailSmtpTransport {
|
|
3
|
+
verify(): Promise<void>;
|
|
4
|
+
sendMail(options: unknown): Promise<{
|
|
5
|
+
messageId?: string;
|
|
6
|
+
}>;
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface EmailImapTransport {
|
|
10
|
+
once(event: 'ready' | 'error', listener: (...args: unknown[]) => void): void;
|
|
11
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
12
|
+
connect(): void;
|
|
13
|
+
end(): void;
|
|
14
|
+
openBox(mailbox: string, openReadWrite: boolean, callback: (error: Error | null, box?: unknown) => void): void;
|
|
15
|
+
search(criteria: string[], callback: (error: Error | null, results: number[]) => void): void;
|
|
16
|
+
fetch(results: number[], options: {
|
|
17
|
+
bodies: string;
|
|
18
|
+
markSeen?: boolean;
|
|
19
|
+
}): {
|
|
20
|
+
on(event: 'message', listener: (msg: EmailImapFetchMessage, seqno: number) => void): void;
|
|
21
|
+
once(event: 'error' | 'end', listener: (error?: Error) => void): void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface EmailImapFetchMessage {
|
|
25
|
+
on(event: 'body', listener: (stream: NodeJS.ReadableStream) => void): void;
|
|
26
|
+
once(event: 'attributes', listener: (attrs: {
|
|
27
|
+
uid?: number;
|
|
28
|
+
}) => void): void;
|
|
29
|
+
once(event: 'end', listener: () => void): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function defaultCreateSmtp(config: ResolvedEmailConfig['smtp']): EmailSmtpTransport;
|
|
32
|
+
export declare function defaultCreateImap(config: ResolvedEmailConfig['imap']): EmailImapTransport;
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email SMTP/IMAP transport factories (nodemailer + imap).
|
|
3
|
+
*/
|
|
4
|
+
import nodemailer from 'nodemailer';
|
|
5
|
+
import Imap from 'imap';
|
|
6
|
+
export function defaultCreateSmtp(config) {
|
|
7
|
+
const transporter = nodemailer.createTransport({
|
|
8
|
+
host: config.host,
|
|
9
|
+
port: config.port,
|
|
10
|
+
secure: config.secure,
|
|
11
|
+
auth: { user: config.auth.user, pass: config.auth.pass },
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
verify: () => transporter.verify().then(() => undefined),
|
|
15
|
+
sendMail: (options) => transporter.sendMail(options),
|
|
16
|
+
close: () => transporter.close(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function defaultCreateImap(config) {
|
|
20
|
+
return new Imap({
|
|
21
|
+
user: config.user,
|
|
22
|
+
password: config.password,
|
|
23
|
+
host: config.host,
|
|
24
|
+
port: config.port,
|
|
25
|
+
tls: config.tls,
|
|
26
|
+
});
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-email",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Zhin.js adapter for
|
|
5
|
+
"description": "Zhin.js Email adapter for Plugin Runtime (SMTP/IMAP)",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"imap": "^0.8.19",
|
|
10
|
-
"mailparser": "^3.9.
|
|
11
|
-
"nodemailer": "^
|
|
10
|
+
"mailparser": "^3.9.14",
|
|
11
|
+
"nodemailer": "^9.0.3",
|
|
12
|
+
"@zhin.js/adapter": "1.1.12",
|
|
13
|
+
"@zhin.js/core": "1.1.35",
|
|
14
|
+
"@zhin.js/feature-kit": "1.1.0",
|
|
15
|
+
"@zhin.js/im-contract": "1.1.0",
|
|
16
|
+
"@zhin.js/logger": "1.1.0"
|
|
12
17
|
},
|
|
13
18
|
"peerDependencies": {
|
|
14
|
-
"zhin.js": "
|
|
19
|
+
"@zhin.js/adapter": "^1.1.12",
|
|
20
|
+
"@zhin.js/core": "^1.1.35",
|
|
21
|
+
"zhin.js": "^1.1.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"zhin.js": {
|
|
25
|
+
"optional": true
|
|
26
|
+
}
|
|
15
27
|
},
|
|
16
28
|
"devDependencies": {
|
|
17
|
-
"@types/imap": "^0.8.
|
|
29
|
+
"@types/imap": "^0.8.43",
|
|
18
30
|
"@types/mailparser": "^3.4.6",
|
|
19
|
-
"@types/node": "^
|
|
20
|
-
"@types/nodemailer": "^8.0.
|
|
31
|
+
"@types/node": "^26.1.2",
|
|
32
|
+
"@types/nodemailer": "^8.0.1",
|
|
21
33
|
"typescript": "^6.0.3",
|
|
22
|
-
"
|
|
34
|
+
"vitest": "^4.1.10",
|
|
35
|
+
"zhin.js": "1.1.0"
|
|
23
36
|
},
|
|
24
37
|
"keywords": [
|
|
25
38
|
"zhin",
|
|
@@ -43,9 +56,12 @@
|
|
|
43
56
|
"directory": "plugins/adapters/email"
|
|
44
57
|
},
|
|
45
58
|
"files": [
|
|
59
|
+
"adapters",
|
|
60
|
+
"plugin.js",
|
|
61
|
+
"schema.json",
|
|
46
62
|
"src",
|
|
47
63
|
"lib",
|
|
48
|
-
"
|
|
64
|
+
"agent",
|
|
49
65
|
"README.md",
|
|
50
66
|
"CHANGELOG.md"
|
|
51
67
|
],
|
|
@@ -60,9 +76,26 @@
|
|
|
60
76
|
"import": "./lib/index.js"
|
|
61
77
|
}
|
|
62
78
|
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
81
|
+
},
|
|
82
|
+
"zhin": {
|
|
83
|
+
"protocol": 1,
|
|
84
|
+
"type": "plugin",
|
|
85
|
+
"entry": "./plugin.js",
|
|
86
|
+
"engine": "^1.0.0",
|
|
87
|
+
"runtime": "trusted",
|
|
88
|
+
"features": [
|
|
89
|
+
{
|
|
90
|
+
"package": "@zhin.js/adapter",
|
|
91
|
+
"api": "^1.0.0"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"plugins": []
|
|
95
|
+
},
|
|
63
96
|
"scripts": {
|
|
64
|
-
"build": "
|
|
97
|
+
"build": "tsc",
|
|
65
98
|
"clean": "rimraf lib",
|
|
66
|
-
"
|
|
99
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/email/tests"
|
|
67
100
|
}
|
|
68
101
|
}
|
package/plugin.js
ADDED
package/schema.json
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
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
|
+
},
|
|
24
|
+
"endpoints": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": true,
|
|
30
|
+
"properties": {
|
|
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 列表"
|
|
48
|
+
},
|
|
49
|
+
"smtp": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"properties": {
|
|
53
|
+
"host": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"port": {
|
|
57
|
+
"type": "number"
|
|
58
|
+
},
|
|
59
|
+
"secure": {
|
|
60
|
+
"type": "boolean"
|
|
61
|
+
},
|
|
62
|
+
"auth": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"additionalProperties": false,
|
|
65
|
+
"properties": {
|
|
66
|
+
"user": {
|
|
67
|
+
"type": "string"
|
|
68
|
+
},
|
|
69
|
+
"pass": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": [
|
|
74
|
+
"user",
|
|
75
|
+
"pass"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"required": [
|
|
80
|
+
"host",
|
|
81
|
+
"port",
|
|
82
|
+
"secure",
|
|
83
|
+
"auth"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"imap": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"additionalProperties": false,
|
|
89
|
+
"properties": {
|
|
90
|
+
"host": {
|
|
91
|
+
"type": "string"
|
|
92
|
+
},
|
|
93
|
+
"port": {
|
|
94
|
+
"type": "number"
|
|
95
|
+
},
|
|
96
|
+
"tls": {
|
|
97
|
+
"type": "boolean"
|
|
98
|
+
},
|
|
99
|
+
"user": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
"password": {
|
|
103
|
+
"type": "string"
|
|
104
|
+
},
|
|
105
|
+
"checkInterval": {
|
|
106
|
+
"type": "number",
|
|
107
|
+
"default": 60000
|
|
108
|
+
},
|
|
109
|
+
"mailbox": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"default": "INBOX"
|
|
112
|
+
},
|
|
113
|
+
"markSeen": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"default": true
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"required": [
|
|
119
|
+
"host",
|
|
120
|
+
"port",
|
|
121
|
+
"tls",
|
|
122
|
+
"user",
|
|
123
|
+
"password"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"attachments": {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"additionalProperties": false,
|
|
129
|
+
"properties": {
|
|
130
|
+
"enabled": {
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"default": false
|
|
133
|
+
},
|
|
134
|
+
"downloadPath": {
|
|
135
|
+
"type": "string"
|
|
136
|
+
},
|
|
137
|
+
"maxFileSize": {
|
|
138
|
+
"type": "number"
|
|
139
|
+
},
|
|
140
|
+
"allowedTypes": {
|
|
141
|
+
"type": "array",
|
|
142
|
+
"items": {
|
|
143
|
+
"type": "string"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"id": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "Email bot name"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"required": [
|
|
154
|
+
"id",
|
|
155
|
+
"smtp",
|
|
156
|
+
"imap"
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"commandPrefix": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"default": "",
|
|
163
|
+
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"required": [
|
|
167
|
+
"endpoints"
|
|
168
|
+
]
|
|
169
|
+
}
|
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');
|