@zhin.js/adapter-email 3.0.2 → 3.0.3

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/src/endpoint.ts CHANGED
@@ -1,425 +1,258 @@
1
1
  /**
2
- * Email Endpoint 实现
2
+ * EmailEndpoint lifecycle, SMTP outbound, IMAP inbound polling.
3
3
  */
4
- import nodemailer from "nodemailer";
5
- import Imap from "imap";
6
- import { simpleParser, type ParsedMail, type Attachment } from "mailparser";
7
- import { formatCompact, Endpoint, Message, MessageSegment, segment, SendContent, SendOptions, htmlToPlainTextWithBlockBreaks, expandInteractiveSegmentsInContent,
8
- } from 'zhin.js';
9
- import { EventEmitter } from "events";
10
- import { createWriteStream, promises as fs } from "fs";
11
- import path from "path";
12
- import type { EmailEndpointConfig, EmailMessage } from "./types.js";
13
- import type { EmailAdapter } from "./adapter.js";
14
- import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
4
+ import { mkdir, writeFile } from 'node:fs/promises';
5
+ import * as path from 'node:path';
6
+ import { simpleParser } from 'mailparser';
7
+ import type { EndpointInstance } from '@zhin.js/adapter';
8
+ import type { MessageGateway } from '@zhin.js/core/runtime';
9
+ import { formatCompact, getLogger } from '@zhin.js/logger';
10
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
11
+ import {
12
+ formatInboundContent,
13
+ formatOutboundMail,
14
+ parseEmailMessage,
15
+ senderDisplayName,
16
+ type EmailMessage,
17
+ type ResolvedEmailConfig,
18
+ } from './protocol.js';
19
+ import {
20
+ defaultCreateImap,
21
+ defaultCreateSmtp,
22
+ type EmailImapFetchMessage,
23
+ type EmailImapTransport,
24
+ type EmailSmtpTransport,
25
+ } from './transport.js';
26
+
27
+ const logger = getLogger('email');
28
+
29
+ export interface EmailEndpointOptions {
30
+ readonly id: CapabilityId;
31
+ readonly gateway: MessageGateway;
32
+ readonly config: ResolvedEmailConfig;
33
+ readonly createSmtp?: (config: ResolvedEmailConfig['smtp']) => EmailSmtpTransport | Promise<EmailSmtpTransport>;
34
+ readonly createImap?: (config: ResolvedEmailConfig['imap']) => EmailImapTransport;
35
+ }
15
36
 
16
- export class EmailEndpoint extends EventEmitter implements Endpoint<EmailEndpointConfig, EmailMessage> {
17
- $config: EmailEndpointConfig;
18
- $connected: boolean = false;
19
- private smtpTransporter: nodemailer.Transporter | null = null;
20
- private imapConnection: Imap | null = null;
21
- private checkTimer: NodeJS.Timeout | null = null;
37
+ export class EmailEndpoint implements EndpointInstance {
38
+ readonly #options: EmailEndpointOptions;
39
+ #smtp: EmailSmtpTransport | null = null;
40
+ #imap: EmailImapTransport | null = null;
41
+ #checkTimer: NodeJS.Timeout | null = null;
42
+ #open = false;
43
+ #started = false;
22
44
 
23
- get logger() {
24
- return this.adapter.plugin.logger;
45
+ constructor(options: EmailEndpointOptions) {
46
+ this.#options = options;
25
47
  }
26
48
 
27
- get $id() {
28
- return this.$config.name;
49
+ async start(): Promise<void> {
50
+ if (this.#started) return;
51
+ this.#started = true;
52
+ const { smtp, imap, name } = this.#options.config;
53
+ try {
54
+ this.#smtp = await (this.#options.createSmtp?.(smtp) ?? defaultCreateSmtp(smtp));
55
+ await this.#smtp.verify();
56
+ logger.debug(formatCompact({ endpoint: name, mode: 'smtp' }));
57
+
58
+ this.#imap = this.#options.createImap?.(imap) ?? defaultCreateImap(imap);
59
+ this.#setupImapListeners(this.#imap);
60
+ await new Promise<void>((resolve, reject) => {
61
+ this.#imap!.once('ready', () => resolve());
62
+ this.#imap!.once('error', (error) => reject(error));
63
+ this.#imap!.connect();
64
+ });
65
+ logger.debug(formatCompact({ endpoint: name, mode: 'imap' }));
66
+ this.#startEmailCheck();
67
+ } catch (error) {
68
+ await this.stop();
69
+ logger.error('Failed to connect email services:', error);
70
+ throw error;
29
71
  }
72
+ }
30
73
 
31
- constructor(public adapter: EmailAdapter, config: EmailEndpointConfig) {
32
- super();
33
- this.$config = config;
74
+ open(): void {
75
+ this.#open = true;
76
+ }
34
77
 
35
- // 设置默认值
36
- this.$config.imap.checkInterval = this.$config.imap.checkInterval || 60000; // 1分钟
37
- this.$config.imap.mailbox = this.$config.imap.mailbox || 'INBOX';
38
- this.$config.imap.markSeen = this.$config.imap.markSeen !== false;
78
+ close(): void {
79
+ this.#open = false;
80
+ }
39
81
 
40
- if (this.$config.attachments?.enabled) {
41
- this.$config.attachments.downloadPath = this.$config.attachments.downloadPath || './downloads/email';
42
- this.$config.attachments.maxFileSize = this.$config.attachments.maxFileSize || 10 * 1024 * 1024; // 10MB
43
- }
82
+ async stop(): Promise<void> {
83
+ this.#open = false;
84
+ if (this.#checkTimer) {
85
+ clearInterval(this.#checkTimer);
86
+ this.#checkTimer = null;
44
87
  }
45
-
46
- async $connect(): Promise<void> {
47
- try {
48
- // 初始化 SMTP 传输器
49
- this.smtpTransporter = nodemailer.createTransport({
50
- host: this.$config.smtp.host,
51
- port: this.$config.smtp.port,
52
- secure: this.$config.smtp.secure,
53
- auth: this.$config.smtp.auth
54
- });
55
-
56
- // 验证 SMTP 连接
57
- await this.smtpTransporter!.verify();
58
- this.logger.debug(formatCompact({ endpoint: this.$id, mode: "smtp" }));
59
-
60
- // 初始化 IMAP 连接
61
- this.imapConnection = new Imap({
62
- user: this.$config.imap.user,
63
- password: this.$config.imap.password,
64
- host: this.$config.imap.host,
65
- port: this.$config.imap.port,
66
- tls: this.$config.imap.tls
67
- });
68
-
69
- // 设置 IMAP 事件监听
70
- this.setupImapListeners();
71
-
72
- // 连接 IMAP
73
- await new Promise<void>((resolve, reject) => {
74
- this.imapConnection!.once('ready', resolve);
75
- this.imapConnection!.once('error', reject);
76
- this.imapConnection!.connect();
77
- });
78
-
79
- this.logger.debug(formatCompact({ endpoint: this.$id, mode: "imap" }));
80
-
81
- // 开始检查邮件
82
- this.startEmailCheck();
83
- this.$connected = true;
84
-
85
- } catch (error) {
86
- this.logger.error('Failed to connect email services:', error);
87
- throw error;
88
- }
88
+ if (this.#imap) {
89
+ try {
90
+ this.#imap.end();
91
+ } catch {
92
+ /* ignore */
93
+ }
94
+ this.#imap = null;
89
95
  }
90
-
91
- async $disconnect(): Promise<void> {
92
- this.$connected = false;
93
-
94
- // 停止定时检查
95
- if (this.checkTimer) {
96
- clearInterval(this.checkTimer);
97
- this.checkTimer = null;
98
- }
99
-
100
- // 关闭 IMAP 连接
101
- if (this.imapConnection) {
102
- this.imapConnection.end();
103
- this.imapConnection = null;
104
- }
105
-
106
- // 关闭 SMTP 连接
107
- if (this.smtpTransporter) {
108
- this.smtpTransporter.close();
109
- this.smtpTransporter = null;
110
- }
111
-
112
- this.logger.debug(formatCompact( { op: "disconnect", endpoint: this.$id }));
96
+ if (this.#smtp) {
97
+ try {
98
+ this.#smtp.close();
99
+ } catch {
100
+ /* ignore */
101
+ }
102
+ this.#smtp = null;
113
103
  }
104
+ this.#started = false;
105
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
106
+ }
114
107
 
115
- private setupImapListeners(): void {
116
- if (!this.imapConnection) return;
117
-
118
- this.imapConnection.on('mail', (numNewMsgs: number) => {
119
- this.logger.debug(`Received ${numNewMsgs} new emails`);
120
- this.checkForNewEmails();
121
- });
122
-
123
- this.imapConnection.on('error', (error: any) => {
124
- this.logger.error('IMAP error:', error);
125
- });
126
-
127
- this.imapConnection.on('end', () => {
128
- this.logger.debug(formatCompact( { op: "disconnect", endpoint: this.$id, mode: "imap" }));
129
- });
130
- }
108
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
109
+ if (!this.#smtp) throw new Error('SMTP transporter not initialized');
110
+ const mailOptions = formatOutboundMail(payload, {
111
+ from: this.#options.config.smtp.auth.user,
112
+ to: target,
113
+ });
114
+ const info = await this.#smtp.sendMail(mailOptions);
115
+ logger.debug(formatCompact({ op: 'email_send', target, messageId: info.messageId }));
116
+ return info.messageId || '';
117
+ }
131
118
 
132
- private startEmailCheck(): void {
133
- if (this.checkTimer) return;
119
+ /** Test / internal: admit a parsed mail when the endpoint is open. */
120
+ admit(email: EmailMessage): void {
121
+ if (!this.#open) return;
122
+ void this.#admitWithAttachments(email).catch((err) => {
123
+ logger.warn(formatCompact({
124
+ op: 'email_gateway_receive_failed',
125
+ target: email.from,
126
+ error: err instanceof Error ? err.message : String(err),
127
+ }));
128
+ });
129
+ }
134
130
 
135
- this.checkTimer = setInterval(() => {
136
- this.checkForNewEmails();
137
- }, this.$config.imap.checkInterval!);
131
+ async #admitWithAttachments(email: EmailMessage): Promise<void> {
132
+ const savedAttachments = await this.#downloadAttachments(email);
133
+ const content = formatInboundContent(email);
134
+ const sender = email.from;
135
+ await this.#options.gateway.receive({
136
+ adapter: this.#options.id,
137
+ target: sender,
138
+ content,
139
+ sender: senderDisplayName(sender),
140
+ id: email.messageId || undefined,
141
+ metadata: Object.freeze({
142
+ subject: email.subject,
143
+ to: email.to,
144
+ cc: email.cc,
145
+ uid: email.uid,
146
+ date: email.date.toISOString(),
147
+ endpoint: this.#options.config.name,
148
+ ...(savedAttachments.length ? { attachments: savedAttachments } : {}),
149
+ }),
150
+ });
151
+ }
138
152
 
139
- // 立即检查一次
140
- this.checkForNewEmails();
153
+ /**
154
+ * attachments.enabled 时把入站附件落盘(恢复旧 downloadAttachment 行为,
155
+ * 附加 maxFileSize / allowedTypes 过滤);返回落盘结果供 admit metadata 使用。
156
+ */
157
+ async #downloadAttachments(
158
+ email: EmailMessage,
159
+ ): Promise<Array<{ filename: string; path: string; contentType?: string; size?: number }>> {
160
+ const config = this.#options.config.attachments;
161
+ if (!config?.enabled || email.attachments.length === 0) return [];
162
+ await mkdir(config.downloadPath, { recursive: true });
163
+ const saved: Array<{ filename: string; path: string; contentType?: string; size?: number }> = [];
164
+ for (const attachment of email.attachments) {
165
+ const filename = attachment.filename || `attachment_${Date.now()}`;
166
+ if (config.allowedTypes?.length && !config.allowedTypes.includes(attachment.contentType ?? '')) {
167
+ logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'type' }));
168
+ continue;
169
+ }
170
+ if (attachment.size != null && attachment.size > config.maxFileSize) {
171
+ logger.debug(formatCompact({ op: 'email_attachment_skipped', filename, reason: 'size' }));
172
+ continue;
173
+ }
174
+ const filepath = path.join(config.downloadPath, filename);
175
+ try {
176
+ await writeFile(filepath, attachment.content);
177
+ saved.push({ filename, path: filepath, contentType: attachment.contentType, size: attachment.size });
178
+ } catch (error) {
179
+ logger.warn(formatCompact({
180
+ op: 'email_attachment_download_failed',
181
+ filename,
182
+ error: error instanceof Error ? error.message : String(error),
183
+ }));
184
+ }
141
185
  }
186
+ return saved;
187
+ }
142
188
 
143
- private async checkForNewEmails(): Promise<void> {
144
- if (!this.imapConnection || !this.$connected) return;
145
-
146
- try {
147
- await new Promise<void>((resolve, reject) => {
148
- this.imapConnection!.openBox(this.$config.imap.mailbox!, false, (error, box) => {
149
- if (error) return reject(error);
150
-
151
- // 搜索未读邮件
152
- this.imapConnection!.search(['UNSEEN'], (error, results) => {
153
- if (error) return reject(error);
154
-
155
- if (results.length === 0) {
156
- return resolve();
157
- }
158
-
159
- // 获取邮件
160
- const fetch = this.imapConnection!.fetch(results, {
161
- bodies: '',
162
- markSeen: this.$config.imap.markSeen
163
- });
189
+ #setupImapListeners(imap: EmailImapTransport): void {
190
+ imap.on('mail', () => {
191
+ void this.#checkForNewEmails();
192
+ });
193
+ imap.on('error', (error) => {
194
+ logger.error('IMAP error:', error);
195
+ });
196
+ imap.on('end', () => {
197
+ logger.debug(formatCompact({
198
+ op: 'disconnect',
199
+ endpoint: this.#options.config.name,
200
+ mode: 'imap',
201
+ }));
202
+ });
203
+ }
164
204
 
165
- fetch.on('message', (msg, seqno) => {
166
- this.handleImapMessage(msg, seqno);
167
- });
205
+ #startEmailCheck(): void {
206
+ if (this.#checkTimer) return;
207
+ this.#checkTimer = setInterval(() => {
208
+ void this.#checkForNewEmails();
209
+ }, this.#options.config.imap.checkInterval);
210
+ void this.#checkForNewEmails();
211
+ }
168
212
 
169
- fetch.once('error', reject);
170
- fetch.once('end', resolve);
171
- });
172
- });
213
+ async #checkForNewEmails(): Promise<void> {
214
+ if (!this.#imap || !this.#started) return;
215
+ try {
216
+ await new Promise<void>((resolve, reject) => {
217
+ this.#imap!.openBox(this.#options.config.imap.mailbox, false, (error) => {
218
+ if (error) return reject(error);
219
+ this.#imap!.search(['UNSEEN'], (searchError, results) => {
220
+ if (searchError) return reject(searchError);
221
+ if (!results.length) return resolve();
222
+ const fetch = this.#imap!.fetch(results, {
223
+ bodies: '',
224
+ markSeen: this.#options.config.imap.markSeen,
173
225
  });
174
- } catch (error) {
175
- this.logger.error('Error checking for new emails:', error);
176
- }
177
- }
178
-
179
- private handleImapMessage(msg: any, seqno: number): void {
180
- let body = '';
181
- let uid = 0;
182
-
183
- msg.on('body', (stream: any) => {
184
- stream.on('data', (chunk: any) => {
185
- body += chunk.toString('utf8');
226
+ fetch.on('message', (msg, seqno) => {
227
+ this.#handleImapMessage(msg, seqno);
186
228
  });
229
+ fetch.once('error', (fetchError) => reject(fetchError));
230
+ fetch.once('end', () => resolve());
231
+ });
187
232
  });
188
-
189
- msg.once('attributes', (attrs: any) => {
190
- uid = attrs.uid;
191
- });
192
-
193
- msg.once('end', async () => {
194
- try {
195
- const parsed = await simpleParser(body);
196
- const emailMessage = this.parseEmailMessage(parsed, uid);
197
- const formattedMessage = this.$formatMessage(emailMessage);
198
- this.adapter.emit('message.receive', formattedMessage);
199
- } catch (error) {
200
- this.logger.error('Error parsing email:', error);
201
- }
202
- });
203
- }
204
-
205
- private parseEmailMessage(parsed: ParsedMail, uid: number): EmailMessage {
206
- const getAddressText = (addr: any): string[] => {
207
- if (!addr) return [];
208
- if (Array.isArray(addr)) {
209
- return addr.map((a: any) => a.text || a.address || a.toString());
210
- }
211
- return [addr.text || addr.address || addr.toString()];
212
- };
213
-
214
- return {
215
- messageId: parsed.messageId || '',
216
- from: parsed.from ? getAddressText(parsed.from)[0] || '' : '',
217
- to: getAddressText(parsed.to),
218
- cc: getAddressText(parsed.cc),
219
- bcc: getAddressText(parsed.bcc),
220
- subject: parsed.subject || '',
221
- text: parsed.text || '',
222
- html: parsed.html ? parsed.html.toString() : '',
223
- attachments: parsed.attachments || [],
224
- date: parsed.date || new Date(),
225
- uid
226
- };
227
- }
228
-
229
- $formatMessage(emailMsg: EmailMessage): Message<EmailMessage> {
230
- // 确定频道类型和ID
231
- const channelType = 'private';
232
- const channelId = emailMsg.from;
233
-
234
- // 解析邮件内容
235
- const wire = EmailEndpoint.parseEmailContent(emailMsg);
236
- const content = toCanonicalSegments(wire);
237
-
238
- const result = Message.from(emailMsg, {
239
- $id: emailMsg.messageId,
240
- $adapter: 'email',
241
- $endpoint: this.$config.name,
242
- $sender: {
243
- id: emailMsg.from,
244
- name: emailMsg.from.split('<')[0].trim() || emailMsg.from
245
- },
246
- $channel: {
247
- id: channelId,
248
- type: channelType as any
249
- },
250
- $raw: JSON.stringify(emailMsg),
251
- $timestamp: emailMsg.date.getTime(),
252
- $content: content,
253
- $recall: async () => {
254
- // 邮件适配器暂时不支持撤回消息
255
- },
256
- $reply: async (content: SendContent): Promise<string> => {
257
- return await this.adapter.sendMessage({
258
- context: this.$config.context,
259
- endpoint: this.$config.name,
260
- id: emailMsg.from,
261
- type: 'private',
262
- content
263
- });
264
- }
265
- });
266
-
267
- return result;
268
- }
269
-
270
- static parseEmailContent(email: EmailMessage): MessageSegment[] {
271
- const segments: MessageSegment[] = [];
272
-
273
- // 添加主题(如果有且不为空)
274
- if (email.subject) {
275
- segments.push(segment.text(`Subject: ${email.subject}\n\n`));
276
- }
277
-
278
- // 添加文本内容
279
- if (email.text) {
280
- segments.push(segment.text(email.text));
281
- }
282
-
283
- // 如果没有纯文本但有HTML,尝试转换
284
- if (!email.text && email.html) {
285
- const textFromHtml = EmailEndpoint.htmlToText(email.html);
286
- if (textFromHtml) {
287
- segments.push(segment.text(textFromHtml));
288
- }
289
- }
290
-
291
- // 处理附件
292
- for (const attachment of email.attachments) {
293
- if (attachment.contentType?.startsWith('image/')) {
294
- segments.push(segment('image', {
295
- filename: attachment.filename,
296
- contentType: attachment.contentType,
297
- size: attachment.size
298
- }));
299
- } else {
300
- segments.push(segment('file', {
301
- filename: attachment.filename,
302
- contentType: attachment.contentType,
303
- size: attachment.size
304
- }));
305
- }
306
- }
307
-
308
- return segments.length > 0 ? segments : [segment.text('(Empty email)')];
309
- }
310
-
311
- async $sendMessage(options: SendOptions): Promise<string> {
312
- if (!this.smtpTransporter) {
313
- throw new Error('SMTP transporter not initialized');
314
- }
315
-
316
- try {
317
- const canonical = expandInteractiveSegmentsInContent(options.content);
318
- const wire = fromCanonicalSegments(canonical);
319
- const mailOptions = await this.formatSendContent({ ...options, content: wire });
320
- const info = await this.smtpTransporter.sendMail(mailOptions);
321
- this.logger.debug('Email sent:', info.messageId);
322
- return info.messageId || '';
323
- } catch (error) {
324
- this.logger.error('Failed to send email:', error);
325
- throw error;
326
- }
327
- }
328
-
329
- async $recallMessage(id: string): Promise<void> {
330
- // 邮件适配器暂时不支持撤回消息
331
- }
332
-
333
- private async formatSendContent(options: SendOptions): Promise<nodemailer.SendMailOptions> {
334
- const mailOptions: nodemailer.SendMailOptions = {
335
- from: this.$config.smtp.auth.user,
336
- to: options.id,
337
- subject: 'Message from Bot'
338
- };
339
-
340
- if (typeof options.content === 'string') {
341
- mailOptions.text = options.content;
342
- } else if (Array.isArray(options.content)) {
343
- const textParts: string[] = [];
344
- const htmlParts: string[] = [];
345
- const attachments: any[] = [];
346
-
347
- for (const item of options.content) {
348
- if (typeof item === 'string') {
349
- textParts.push(item);
350
- htmlParts.push(item.replace(/\n/g, '<br>'));
351
- } else {
352
- const segment = item as MessageSegment;
353
- switch (segment.type) {
354
- case 'text':
355
- const textContent = segment.data.text || segment.data.content || '';
356
- textParts.push(textContent);
357
- htmlParts.push(textContent.replace(/\n/g, '<br>'));
358
- break;
359
- case 'image':
360
- if (segment.data.url) {
361
- attachments.push({
362
- filename: segment.data.filename || 'image.png',
363
- path: segment.data.url
364
- });
365
- }
366
- break;
367
- case 'file':
368
- if (segment.data.url) {
369
- attachments.push({
370
- filename: segment.data.filename || 'file',
371
- path: segment.data.url
372
- });
373
- }
374
- break;
375
- }
376
- }
377
- }
378
-
379
- if (textParts.length > 0) {
380
- mailOptions.text = textParts.join('\n');
381
- mailOptions.html = htmlParts.join('<br>');
382
- }
383
-
384
- if (attachments.length > 0) {
385
- mailOptions.attachments = attachments;
386
- }
387
- }
388
-
389
- // 如果有回复对象,可以在这里处理
390
- // 邮件适配器暂时不支持回复对象
391
-
392
- return mailOptions;
393
- }
394
-
395
- // 下载附件到本地
396
- private async downloadAttachment(attachment: Attachment): Promise<string> {
397
- if (!this.$config.attachments?.enabled || !this.$config.attachments.downloadPath) {
398
- throw new Error('Attachment download is not enabled');
399
- }
400
-
401
- const downloadPath = this.$config.attachments.downloadPath;
402
- await fs.mkdir(downloadPath, { recursive: true });
403
-
404
- const filename = attachment.filename || `attachment_${Date.now()}`;
405
- const filepath = path.join(downloadPath, filename);
406
-
407
- return new Promise((resolve, reject) => {
408
- const writeStream = createWriteStream(filepath);
409
- writeStream.write(attachment.content);
410
- writeStream.end();
411
-
412
- writeStream.on('finish', () => resolve(filepath));
413
- writeStream.on('error', reject);
414
- });
233
+ });
234
+ } catch (error) {
235
+ logger.error('Error checking for new emails:', error);
415
236
  }
237
+ }
416
238
 
417
- /**
418
- * HTML 纯文本转换,处理常见标签和实体
419
- */
420
- static htmlToText(html: string): string {
421
- return htmlToPlainTextWithBlockBreaks(html);
422
- }
239
+ #handleImapMessage(msg: EmailImapFetchMessage, _seqno: number): void {
240
+ let body = '';
241
+ let uid = 0;
242
+ msg.on('body', (stream) => {
243
+ stream.on('data', (chunk: Buffer | string) => {
244
+ body += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
245
+ });
246
+ });
247
+ msg.once('attributes', (attrs) => {
248
+ uid = attrs.uid ?? 0;
249
+ });
250
+ msg.once('end', () => {
251
+ void simpleParser(body).then((parsed) => {
252
+ this.admit(parseEmailMessage(parsed, uid));
253
+ }).catch((error) => {
254
+ logger.error('Error parsing email:', error);
255
+ });
256
+ });
257
+ }
423
258
  }
424
-
425
- // 创建和注册适配器