@builderbot/provider-email 1.3.15-alpha.8

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/dist/index.cjs ADDED
@@ -0,0 +1,871 @@
1
+ 'use strict';
2
+
3
+ var bot = require('@builderbot/bot');
4
+ var promises = require('fs/promises');
5
+ var os = require('os');
6
+ var path = require('path');
7
+ var imapflow = require('imapflow');
8
+ var mailparser = require('mailparser');
9
+ var EventEmitter = require('node:events');
10
+ var nodemailer = require('nodemailer');
11
+
12
+ /**
13
+ * Class representing EmailCoreVendor, handles IMAP/SMTP operations.
14
+ * @extends EventEmitter
15
+ */
16
+ class EmailCoreVendor extends EventEmitter {
17
+ constructor(config) {
18
+ super();
19
+ this.imapClient = null;
20
+ this.smtpTransporter = null;
21
+ this.isConnected = false;
22
+ this.reconnectAttempts = 0;
23
+ this.maxReconnectAttempts = 10;
24
+ this.reconnectDelay = 5000;
25
+ this.config = config;
26
+ this.initializeSmtp();
27
+ }
28
+ /**
29
+ * Initialize SMTP transporter for sending emails
30
+ */
31
+ initializeSmtp() {
32
+ try {
33
+ this.smtpTransporter = nodemailer.createTransport({
34
+ host: this.config.smtp.host,
35
+ port: this.config.smtp.port,
36
+ secure: this.config.smtp.secure ?? true,
37
+ auth: {
38
+ user: this.config.smtp.auth.user,
39
+ pass: this.config.smtp.auth.pass,
40
+ },
41
+ });
42
+ console.log('[EmailProvider] SMTP transporter initialized');
43
+ }
44
+ catch (error) {
45
+ console.error('[EmailProvider] Failed to initialize SMTP:', error);
46
+ this.emit('auth_failure', error);
47
+ }
48
+ }
49
+ /**
50
+ * Connect to IMAP server and start listening for new emails
51
+ */
52
+ async connect() {
53
+ try {
54
+ this.imapClient = new imapflow.ImapFlow({
55
+ host: this.config.imap.host,
56
+ port: this.config.imap.port,
57
+ secure: this.config.imap.secure ?? true,
58
+ auth: {
59
+ user: this.config.imap.auth.user,
60
+ pass: this.config.imap.auth.pass,
61
+ },
62
+ logger: false,
63
+ });
64
+ // Handle connection events
65
+ this.imapClient.on('error', (err) => {
66
+ console.error('[EmailProvider] IMAP error:', err);
67
+ this.emit('error', err);
68
+ this.handleDisconnect();
69
+ });
70
+ this.imapClient.on('close', () => {
71
+ console.log('[EmailProvider] IMAP connection closed');
72
+ this.isConnected = false;
73
+ this.handleDisconnect();
74
+ });
75
+ await this.imapClient.connect();
76
+ this.isConnected = true;
77
+ this.reconnectAttempts = 0;
78
+ console.log('[EmailProvider] Connected to IMAP server');
79
+ const host = {
80
+ email: this.config.imap.auth.user,
81
+ phone: this.config.imap.auth.user,
82
+ };
83
+ this.emit('host', host);
84
+ this.emit('ready');
85
+ // Start listening for new emails
86
+ await this.startIdleListener();
87
+ }
88
+ catch (error) {
89
+ console.error('[EmailProvider] Failed to connect to IMAP:', error);
90
+ this.emit('auth_failure', error);
91
+ throw error;
92
+ }
93
+ }
94
+ /**
95
+ * Handle disconnection and attempt reconnection
96
+ */
97
+ async handleDisconnect() {
98
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
99
+ console.error('[EmailProvider] Max reconnection attempts reached');
100
+ this.emit('auth_failure', new Error('Max reconnection attempts reached'));
101
+ return;
102
+ }
103
+ this.reconnectAttempts++;
104
+ const delay = this.reconnectDelay * this.reconnectAttempts;
105
+ console.log(`[EmailProvider] Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
106
+ setTimeout(async () => {
107
+ try {
108
+ await this.connect();
109
+ }
110
+ catch (error) {
111
+ console.error('[EmailProvider] Reconnection failed:', error);
112
+ }
113
+ }, delay);
114
+ }
115
+ /**
116
+ * Start IMAP IDLE listener for real-time email notifications
117
+ */
118
+ async startIdleListener() {
119
+ if (!this.imapClient || !this.isConnected)
120
+ return;
121
+ const mailbox = this.config.mailbox || 'INBOX';
122
+ try {
123
+ // Select the mailbox
124
+ const lock = await this.imapClient.getMailboxLock(mailbox);
125
+ try {
126
+ // Listen for new messages using EXISTS event
127
+ this.imapClient.on('exists', async (data) => {
128
+ if (data.count > data.prevCount) {
129
+ console.log(`[EmailProvider] New email detected in ${data.path}`);
130
+ await this.fetchNewEmails(data.prevCount + 1, data.count);
131
+ }
132
+ });
133
+ // Start IDLE mode
134
+ console.log(`[EmailProvider] Starting IDLE mode on ${mailbox}`);
135
+ // Keep the connection alive with IDLE
136
+ while (this.isConnected && this.imapClient) {
137
+ try {
138
+ await this.imapClient.idle();
139
+ }
140
+ catch (idleError) {
141
+ if (this.isConnected) {
142
+ console.error('[EmailProvider] IDLE error:', idleError);
143
+ }
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ finally {
149
+ lock.release();
150
+ }
151
+ }
152
+ catch (error) {
153
+ console.error('[EmailProvider] Failed to start IDLE listener:', error);
154
+ this.emit('error', error);
155
+ }
156
+ }
157
+ /**
158
+ * Fetch new emails from a sequence range
159
+ */
160
+ async fetchNewEmails(startSeq, endSeq) {
161
+ if (!this.imapClient || !this.isConnected)
162
+ return;
163
+ const mailbox = this.config.mailbox || 'INBOX';
164
+ try {
165
+ const lock = await this.imapClient.getMailboxLock(mailbox);
166
+ try {
167
+ for await (const message of this.imapClient.fetch(`${startSeq}:${endSeq}`, {
168
+ source: true,
169
+ uid: true,
170
+ })) {
171
+ try {
172
+ const parsed = await mailparser.simpleParser(message.source);
173
+ const emailContext = this.parseEmailToContext(parsed, message.uid);
174
+ if (emailContext) {
175
+ // Mark as read if configured
176
+ if (this.config.markAsRead !== false) {
177
+ await this.imapClient.messageFlagsAdd({ uid: message.uid }, ['\\Seen']);
178
+ }
179
+ this.emit('message', emailContext);
180
+ }
181
+ }
182
+ catch (parseError) {
183
+ console.error('[EmailProvider] Failed to parse email:', parseError);
184
+ }
185
+ }
186
+ }
187
+ finally {
188
+ lock.release();
189
+ }
190
+ }
191
+ catch (error) {
192
+ console.error('[EmailProvider] Failed to fetch new emails:', error);
193
+ }
194
+ }
195
+ /**
196
+ * Parse a mailparser ParsedMail object to EmailBotContext
197
+ */
198
+ parseEmailToContext(parsed, uid) {
199
+ const fromAddress = this.extractAddress(parsed.from);
200
+ if (!fromAddress) {
201
+ console.warn('[EmailProvider] Email has no from address, skipping');
202
+ return null;
203
+ }
204
+ // Extract attachments
205
+ const attachments = (parsed.attachments || []).map((att) => ({
206
+ filename: att.filename || 'unnamed',
207
+ contentType: att.contentType,
208
+ size: att.size,
209
+ contentId: att.contentId,
210
+ content: att.content,
211
+ }));
212
+ // Determine if this is a reply
213
+ const isReply = !!(parsed.inReplyTo || (parsed.references && parsed.references.length > 0));
214
+ // Get thread ID from references
215
+ const threadId = parsed.references
216
+ ? Array.isArray(parsed.references)
217
+ ? parsed.references[0]
218
+ : parsed.references
219
+ : parsed.inReplyTo || parsed.messageId;
220
+ // Build body - generate special events for attachments
221
+ let body = parsed.text || '';
222
+ if (attachments.length > 0 && !body.trim()) {
223
+ // Email with only attachments
224
+ const hasMedia = attachments.some((a) => a.contentType.startsWith('image/') || a.contentType.startsWith('video/'));
225
+ const hasDocument = attachments.some((a) => a.contentType.startsWith('application/') || a.contentType.startsWith('text/'));
226
+ if (hasMedia) {
227
+ body = bot.utils.generateRefProvider('_event_media_');
228
+ }
229
+ else if (hasDocument) {
230
+ body = bot.utils.generateRefProvider('_event_document_');
231
+ }
232
+ }
233
+ const context = {
234
+ from: fromAddress.address,
235
+ name: fromAddress.name || fromAddress.address,
236
+ body: body,
237
+ subject: parsed.subject || '(no subject)',
238
+ messageId: parsed.messageId || `${uid}@${this.config.imap.host}`,
239
+ threadId: threadId,
240
+ inReplyTo: parsed.inReplyTo,
241
+ attachments: attachments.length > 0 ? attachments : undefined,
242
+ isReply: isReply,
243
+ html: parsed.html || undefined,
244
+ to: this.extractAddresses(parsed.to),
245
+ cc: parsed.cc ? this.extractAddresses(parsed.cc) : undefined,
246
+ date: parsed.date,
247
+ uid: uid,
248
+ };
249
+ return context;
250
+ }
251
+ /**
252
+ * Extract single address from AddressObject
253
+ */
254
+ extractAddress(addressObj) {
255
+ if (!addressObj)
256
+ return null;
257
+ const obj = Array.isArray(addressObj) ? addressObj[0] : addressObj;
258
+ if (!obj || !obj.value || obj.value.length === 0)
259
+ return null;
260
+ const first = obj.value[0];
261
+ return {
262
+ address: first.address || '',
263
+ name: first.name || '',
264
+ };
265
+ }
266
+ /**
267
+ * Extract array of addresses from AddressObject
268
+ */
269
+ extractAddresses(addressObj) {
270
+ if (!addressObj)
271
+ return [];
272
+ const objects = Array.isArray(addressObj) ? addressObj : [addressObj];
273
+ const addresses = [];
274
+ for (const obj of objects) {
275
+ if (obj && obj.value) {
276
+ for (const addr of obj.value) {
277
+ if (addr.address) {
278
+ addresses.push(addr.address);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ return addresses;
284
+ }
285
+ /**
286
+ * Send an email via SMTP
287
+ */
288
+ async sendEmail(to, subject, text, options) {
289
+ if (!this.smtpTransporter) {
290
+ throw new Error('SMTP transporter not initialized');
291
+ }
292
+ const fromEmail = this.config.fromEmail || this.config.smtp.auth.user;
293
+ const fromName = this.config.fromName || fromEmail;
294
+ const mailOptions = {
295
+ from: `"${fromName}" <${fromEmail}>`,
296
+ to: to,
297
+ subject: subject,
298
+ text: text,
299
+ };
300
+ // Add optional fields
301
+ if (options?.html) {
302
+ mailOptions.html = options.html;
303
+ }
304
+ if (options?.cc) {
305
+ mailOptions.cc = options.cc;
306
+ }
307
+ if (options?.bcc) {
308
+ mailOptions.bcc = options.bcc;
309
+ }
310
+ if (options?.replyTo) {
311
+ mailOptions.replyTo = options.replyTo;
312
+ }
313
+ if (options?.inReplyTo) {
314
+ mailOptions.inReplyTo = options.inReplyTo;
315
+ }
316
+ if (options?.references) {
317
+ mailOptions.references = Array.isArray(options.references)
318
+ ? options.references.join(' ')
319
+ : options.references;
320
+ }
321
+ if (options?.attachments) {
322
+ mailOptions.attachments = options.attachments.map((att) => ({
323
+ filename: att.filename,
324
+ path: att.path,
325
+ content: att.content,
326
+ contentType: att.contentType,
327
+ }));
328
+ }
329
+ try {
330
+ const info = await this.smtpTransporter.sendMail(mailOptions);
331
+ console.log(`[EmailProvider] Email sent: ${info.messageId}`);
332
+ return { messageId: info.messageId };
333
+ }
334
+ catch (error) {
335
+ console.error('[EmailProvider] Failed to send email:', error);
336
+ throw error;
337
+ }
338
+ }
339
+ /**
340
+ * Reply to an existing email thread
341
+ */
342
+ async replyToEmail(originalContext, text, options) {
343
+ // Build references chain
344
+ const references = [];
345
+ if (originalContext.threadId) {
346
+ references.push(originalContext.threadId);
347
+ }
348
+ if (originalContext.messageId && originalContext.messageId !== originalContext.threadId) {
349
+ references.push(originalContext.messageId);
350
+ }
351
+ // Prepare subject with Re: prefix if not already present
352
+ let subject = originalContext.subject;
353
+ if (!subject.toLowerCase().startsWith('re:')) {
354
+ subject = `Re: ${subject}`;
355
+ }
356
+ return this.sendEmail(originalContext.from, subject, text, {
357
+ ...options,
358
+ inReplyTo: originalContext.messageId,
359
+ references: references,
360
+ });
361
+ }
362
+ /**
363
+ * Download attachment content
364
+ */
365
+ async downloadAttachment(ctx, attachmentIndex) {
366
+ if (!ctx.attachments || attachmentIndex >= ctx.attachments.length) {
367
+ return null;
368
+ }
369
+ const attachment = ctx.attachments[attachmentIndex];
370
+ if (attachment.content) {
371
+ return attachment.content;
372
+ }
373
+ // Attachment content should already be in memory from parsing
374
+ console.warn('[EmailProvider] Attachment content not available');
375
+ return null;
376
+ }
377
+ /**
378
+ * Disconnect from IMAP server
379
+ */
380
+ async disconnect() {
381
+ this.isConnected = false;
382
+ if (this.imapClient) {
383
+ try {
384
+ await this.imapClient.logout();
385
+ }
386
+ catch (error) {
387
+ console.error('[EmailProvider] Error during logout:', error);
388
+ }
389
+ this.imapClient = null;
390
+ }
391
+ if (this.smtpTransporter) {
392
+ this.smtpTransporter.close();
393
+ this.smtpTransporter = null;
394
+ }
395
+ console.log('[EmailProvider] Disconnected');
396
+ }
397
+ /**
398
+ * Check if connected to IMAP server
399
+ */
400
+ isImapConnected() {
401
+ return this.isConnected && this.imapClient !== null;
402
+ }
403
+ /**
404
+ * Verify SMTP connection
405
+ */
406
+ async verifySmtp() {
407
+ if (!this.smtpTransporter)
408
+ return false;
409
+ try {
410
+ await this.smtpTransporter.verify();
411
+ return true;
412
+ }
413
+ catch {
414
+ return false;
415
+ }
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Email Provider for BuilderBot
421
+ * Supports receiving emails via IMAP (with IDLE) and sending via SMTP
422
+ * @extends ProviderClass
423
+ */
424
+ class EmailProvider extends bot.ProviderClass {
425
+ constructor(args) {
426
+ super();
427
+ /**
428
+ * Index home endpoint
429
+ */
430
+ this.indexHome = (_, res) => {
431
+ res.end('Email Provider running');
432
+ };
433
+ /**
434
+ * Webhook handler for external email notifications (optional)
435
+ */
436
+ this.webhookHandler = (req, res) => {
437
+ // This can be used for external email webhook integrations
438
+ const body = req.body;
439
+ console.log('[EmailProvider] Webhook received:', body);
440
+ res.end(JSON.stringify({ status: 'ok' }));
441
+ };
442
+ /**
443
+ * Map vendor events to provider events
444
+ */
445
+ this.busEvents = () => [
446
+ {
447
+ event: 'auth_failure',
448
+ func: (payload) => this.emit('auth_failure', payload),
449
+ },
450
+ {
451
+ event: 'ready',
452
+ func: () => this.emit('ready', true),
453
+ },
454
+ {
455
+ event: 'message',
456
+ func: (payload) => {
457
+ this.emit('message', payload);
458
+ },
459
+ },
460
+ {
461
+ event: 'host',
462
+ func: (payload) => {
463
+ this.emit('host', payload);
464
+ },
465
+ },
466
+ {
467
+ event: 'error',
468
+ func: (payload) => {
469
+ console.error('[EmailProvider] Error:', payload);
470
+ },
471
+ },
472
+ ];
473
+ // Validate required configuration
474
+ if (!args.imap) {
475
+ throw new Error('IMAP configuration is required');
476
+ }
477
+ if (!args.smtp) {
478
+ throw new Error('SMTP configuration is required');
479
+ }
480
+ if (!args.imap.host || !args.imap.auth?.user || !args.imap.auth?.pass) {
481
+ throw new Error('IMAP host and authentication are required');
482
+ }
483
+ if (!args.smtp.host || !args.smtp.auth?.user || !args.smtp.auth?.pass) {
484
+ throw new Error('SMTP host and authentication are required');
485
+ }
486
+ this.globalVendorArgs = {
487
+ name: 'email-bot',
488
+ port: 3000,
489
+ writeMyself: 'none',
490
+ mailbox: 'INBOX',
491
+ markAsRead: true,
492
+ ...args,
493
+ };
494
+ }
495
+ /**
496
+ * Initialize the email vendor (IMAP/SMTP connections)
497
+ */
498
+ async initVendor() {
499
+ const vendor = new EmailCoreVendor(this.globalVendorArgs);
500
+ this.vendor = vendor;
501
+ // Connect to IMAP server
502
+ await vendor.connect();
503
+ return vendor;
504
+ }
505
+ /**
506
+ * Called before HTTP server initialization
507
+ */
508
+ beforeHttpServerInit() {
509
+ this.server = this.server
510
+ .use((req, _, next) => {
511
+ req['globalVendorArgs'] = this.globalVendorArgs;
512
+ return next();
513
+ })
514
+ .get('/', this.indexHome)
515
+ .post('/webhook', this.webhookHandler);
516
+ }
517
+ /**
518
+ * Called after HTTP server initialization
519
+ */
520
+ afterHttpServerInit() { }
521
+ /**
522
+ * Send an email message
523
+ * @param to - Recipient email address
524
+ * @param message - Email body content
525
+ * @param options - Send options (subject, attachments, etc.)
526
+ */
527
+ async sendMessage(to, message, options) {
528
+ const emailOptions = options;
529
+ // Default subject if not provided
530
+ const subject = emailOptions?.subject || 'Message from Bot';
531
+ // Check if we're replying to an existing thread
532
+ if (emailOptions?.inReplyTo) {
533
+ return this.vendor.sendEmail(to, subject, message, emailOptions);
534
+ }
535
+ // Check for media/attachments
536
+ if (options?.media) {
537
+ return this.sendMedia(to, message, options.media, emailOptions);
538
+ }
539
+ return this.vendor.sendEmail(to, subject, message, emailOptions);
540
+ }
541
+ /**
542
+ * Send an email with media attachment
543
+ * @param to - Recipient email address
544
+ * @param message - Email body content
545
+ * @param mediaPath - Path to media file
546
+ * @param options - Additional email options
547
+ */
548
+ async sendMedia(to, message, mediaPath, options) {
549
+ const subject = options?.subject || 'Message with attachment';
550
+ const attachments = [
551
+ {
552
+ filename: mediaPath.split('/').pop() || 'attachment',
553
+ path: mediaPath,
554
+ },
555
+ ];
556
+ return this.vendor.sendEmail(to, subject, message, {
557
+ ...options,
558
+ attachments: [...(options?.attachments || []), ...attachments],
559
+ });
560
+ }
561
+ /**
562
+ * Reply to an existing email thread
563
+ * @param ctx - Original email context
564
+ * @param message - Reply message content
565
+ * @param options - Additional email options
566
+ */
567
+ async reply(ctx, message, options) {
568
+ return this.vendor.replyToEmail(ctx, message, options);
569
+ }
570
+ /**
571
+ * Save an attachment from an email to disk
572
+ * @param ctx - Email context containing attachments
573
+ * @param options - Save options (path, attachment index)
574
+ */
575
+ async saveFile(ctx, options) {
576
+ try {
577
+ const emailCtx = ctx;
578
+ if (!emailCtx.attachments || emailCtx.attachments.length === 0) {
579
+ throw new Error('No attachments in email');
580
+ }
581
+ const attachmentIndex = options?.attachmentIndex ?? 0;
582
+ const attachment = emailCtx.attachments[attachmentIndex];
583
+ if (!attachment) {
584
+ throw new Error(`Attachment at index ${attachmentIndex} not found`);
585
+ }
586
+ if (!attachment.content) {
587
+ throw new Error('Attachment content not available');
588
+ }
589
+ const savePath = options?.path ?? os.tmpdir();
590
+ const fileName = `${Date.now()}-${attachment.filename}`;
591
+ const filePath = path.join(savePath, fileName);
592
+ await promises.writeFile(filePath, attachment.content);
593
+ return path.resolve(filePath);
594
+ }
595
+ catch (error) {
596
+ console.error('[EmailProvider] Error saving file:', error);
597
+ throw error;
598
+ }
599
+ }
600
+ /**
601
+ * Get all attachments from an email
602
+ * @param ctx - Email context
603
+ */
604
+ getAttachments(ctx) {
605
+ return ctx.attachments || [];
606
+ }
607
+ /**
608
+ * Check if the email is a reply
609
+ * @param ctx - Email context
610
+ */
611
+ isReply(ctx) {
612
+ return ctx.isReply;
613
+ }
614
+ /**
615
+ * Get the thread ID from an email
616
+ * @param ctx - Email context
617
+ */
618
+ getThreadId(ctx) {
619
+ return ctx.threadId;
620
+ }
621
+ /**
622
+ * Disconnect the email provider
623
+ */
624
+ async disconnect() {
625
+ if (this.vendor) {
626
+ await this.vendor.disconnect();
627
+ }
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Email parsing utilities
633
+ */
634
+ /**
635
+ * Extract email address from a string that might contain name and email
636
+ * e.g., "John Doe <john@example.com>" -> "john@example.com"
637
+ */
638
+ function extractEmailAddress(input) {
639
+ if (!input)
640
+ return '';
641
+ // Check if it contains angle brackets
642
+ const match = input.match(/<([^>]+)>/);
643
+ if (match) {
644
+ return match[1].trim().toLowerCase();
645
+ }
646
+ // Return as-is if it looks like an email
647
+ const trimmed = input.trim().toLowerCase();
648
+ if (isValidEmail(trimmed)) {
649
+ return trimmed;
650
+ }
651
+ return trimmed;
652
+ }
653
+ /**
654
+ * Extract name from email string
655
+ * e.g., "John Doe <john@example.com>" -> "John Doe"
656
+ */
657
+ function extractEmailName(input) {
658
+ if (!input)
659
+ return '';
660
+ // Check if it contains angle brackets
661
+ const bracketIndex = input.indexOf('<');
662
+ if (bracketIndex > 0) {
663
+ return input
664
+ .substring(0, bracketIndex)
665
+ .trim()
666
+ .replace(/^["']|["']$/g, '');
667
+ }
668
+ return '';
669
+ }
670
+ /**
671
+ * Validate email address format
672
+ */
673
+ function isValidEmail(email) {
674
+ if (!email)
675
+ return false;
676
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
677
+ return emailRegex.test(email);
678
+ }
679
+ /**
680
+ * Clean and normalize email address
681
+ */
682
+ function cleanEmail(email) {
683
+ return extractEmailAddress(email).toLowerCase().trim();
684
+ }
685
+ /**
686
+ * Parse email list (comma or semicolon separated)
687
+ */
688
+ function parseEmailList(input) {
689
+ if (!input)
690
+ return [];
691
+ return input
692
+ .split(/[,;]/)
693
+ .map((email) => extractEmailAddress(email))
694
+ .filter((email) => isValidEmail(email));
695
+ }
696
+ /**
697
+ * Format email address with optional name
698
+ */
699
+ function formatEmailAddress(email, name) {
700
+ if (name) {
701
+ return `"${name}" <${email}>`;
702
+ }
703
+ return email;
704
+ }
705
+ /**
706
+ * Extract plain text from HTML email content
707
+ * Basic implementation - strips HTML tags
708
+ */
709
+ function htmlToText(html) {
710
+ if (!html)
711
+ return '';
712
+ return (html
713
+ // Remove script and style tags with content
714
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
715
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
716
+ // Replace common block elements with newlines
717
+ .replace(/<\/?(div|p|br|h[1-6]|li|tr)[^>]*>/gi, '\n')
718
+ // Remove remaining HTML tags
719
+ .replace(/<[^>]+>/g, '')
720
+ // Decode common HTML entities
721
+ .replace(/&nbsp;/gi, ' ')
722
+ .replace(/&amp;/gi, '&')
723
+ .replace(/&lt;/gi, '<')
724
+ .replace(/&gt;/gi, '>')
725
+ .replace(/&quot;/gi, '"')
726
+ .replace(/&#39;/gi, "'")
727
+ // Clean up whitespace
728
+ .replace(/\n\s*\n/g, '\n\n')
729
+ .trim());
730
+ }
731
+ /**
732
+ * Check if content is likely HTML
733
+ */
734
+ function isHtml(content) {
735
+ if (!content)
736
+ return false;
737
+ return /<[a-z][\s\S]*>/i.test(content);
738
+ }
739
+ /**
740
+ * Extract thread ID from References or In-Reply-To headers
741
+ */
742
+ function extractThreadId(references, inReplyTo) {
743
+ // Try references first (get the first/root message)
744
+ if (references) {
745
+ if (Array.isArray(references) && references.length > 0) {
746
+ return references[0];
747
+ }
748
+ if (typeof references === 'string') {
749
+ const refs = references.split(/\s+/).filter(Boolean);
750
+ if (refs.length > 0)
751
+ return refs[0];
752
+ }
753
+ }
754
+ // Fall back to In-Reply-To
755
+ if (inReplyTo) {
756
+ return inReplyTo;
757
+ }
758
+ return undefined;
759
+ }
760
+ /**
761
+ * Check if email subject indicates a reply
762
+ */
763
+ function isReplySubject(subject) {
764
+ if (!subject)
765
+ return false;
766
+ const replyPrefixes = ['re:', 'r:', 'aw:', 'sv:', 'antw:', 'odp:'];
767
+ const lowerSubject = subject.toLowerCase().trim();
768
+ return replyPrefixes.some((prefix) => lowerSubject.startsWith(prefix));
769
+ }
770
+ /**
771
+ * Strip reply prefixes from subject
772
+ */
773
+ function stripReplyPrefix(subject) {
774
+ if (!subject)
775
+ return '';
776
+ return subject.replace(/^(re:|r:|aw:|sv:|antw:|odp:)\s*/i, '').trim();
777
+ }
778
+ /**
779
+ * Add reply prefix to subject if not present
780
+ */
781
+ function addReplyPrefix(subject) {
782
+ if (!subject)
783
+ return 'Re:';
784
+ if (isReplySubject(subject))
785
+ return subject;
786
+ return `Re: ${subject}`;
787
+ }
788
+ /**
789
+ * Generate a unique message ID
790
+ */
791
+ function generateMessageId(domain) {
792
+ const timestamp = Date.now().toString(36);
793
+ const random = Math.random().toString(36).substring(2, 10);
794
+ return `<${timestamp}.${random}@${domain}>`;
795
+ }
796
+ /**
797
+ * Parse MIME content type
798
+ */
799
+ function parseMimeType(contentType) {
800
+ if (!contentType) {
801
+ return { type: 'text', subtype: 'plain', parameters: {} };
802
+ }
803
+ const parts = contentType.split(';');
804
+ const [type, subtype] = (parts[0] || 'text/plain').split('/');
805
+ const parameters = {};
806
+ for (let i = 1; i < parts.length; i++) {
807
+ const param = parts[i].trim();
808
+ const eqIndex = param.indexOf('=');
809
+ if (eqIndex > 0) {
810
+ const key = param.substring(0, eqIndex).trim().toLowerCase();
811
+ let value = param.substring(eqIndex + 1).trim();
812
+ // Remove quotes
813
+ if (value.startsWith('"') && value.endsWith('"')) {
814
+ value = value.slice(1, -1);
815
+ }
816
+ parameters[key] = value;
817
+ }
818
+ }
819
+ return {
820
+ type: type?.toLowerCase() || 'text',
821
+ subtype: subtype?.toLowerCase() || 'plain',
822
+ parameters,
823
+ };
824
+ }
825
+ /**
826
+ * Get file extension from MIME type
827
+ */
828
+ function mimeToExtension(mimeType) {
829
+ const mimeMap = {
830
+ 'text/plain': 'txt',
831
+ 'text/html': 'html',
832
+ 'text/css': 'css',
833
+ 'text/javascript': 'js',
834
+ 'application/json': 'json',
835
+ 'application/pdf': 'pdf',
836
+ 'application/zip': 'zip',
837
+ 'application/xml': 'xml',
838
+ 'image/jpeg': 'jpg',
839
+ 'image/png': 'png',
840
+ 'image/gif': 'gif',
841
+ 'image/webp': 'webp',
842
+ 'image/svg+xml': 'svg',
843
+ 'audio/mpeg': 'mp3',
844
+ 'audio/wav': 'wav',
845
+ 'audio/ogg': 'ogg',
846
+ 'video/mp4': 'mp4',
847
+ 'video/webm': 'webm',
848
+ 'video/quicktime': 'mov',
849
+ };
850
+ const { type, subtype } = parseMimeType(mimeType);
851
+ const fullType = `${type}/${subtype}`;
852
+ return mimeMap[fullType] || subtype || 'bin';
853
+ }
854
+
855
+ exports.EmailCoreVendor = EmailCoreVendor;
856
+ exports.EmailProvider = EmailProvider;
857
+ exports.addReplyPrefix = addReplyPrefix;
858
+ exports.cleanEmail = cleanEmail;
859
+ exports.extractEmailAddress = extractEmailAddress;
860
+ exports.extractEmailName = extractEmailName;
861
+ exports.extractThreadId = extractThreadId;
862
+ exports.formatEmailAddress = formatEmailAddress;
863
+ exports.generateMessageId = generateMessageId;
864
+ exports.htmlToText = htmlToText;
865
+ exports.isHtml = isHtml;
866
+ exports.isReplySubject = isReplySubject;
867
+ exports.isValidEmail = isValidEmail;
868
+ exports.mimeToExtension = mimeToExtension;
869
+ exports.parseEmailList = parseEmailList;
870
+ exports.parseMimeType = parseMimeType;
871
+ exports.stripReplyPrefix = stripReplyPrefix;