@auxiora/channels 1.0.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.
Files changed (89) hide show
  1. package/LICENSE +191 -0
  2. package/dist/adapters/bluebubbles.d.ts +63 -0
  3. package/dist/adapters/bluebubbles.d.ts.map +1 -0
  4. package/dist/adapters/bluebubbles.js +197 -0
  5. package/dist/adapters/bluebubbles.js.map +1 -0
  6. package/dist/adapters/discord.d.ts +27 -0
  7. package/dist/adapters/discord.d.ts.map +1 -0
  8. package/dist/adapters/discord.js +202 -0
  9. package/dist/adapters/discord.js.map +1 -0
  10. package/dist/adapters/email.d.ts +39 -0
  11. package/dist/adapters/email.d.ts.map +1 -0
  12. package/dist/adapters/email.js +359 -0
  13. package/dist/adapters/email.js.map +1 -0
  14. package/dist/adapters/googlechat.d.ts +77 -0
  15. package/dist/adapters/googlechat.d.ts.map +1 -0
  16. package/dist/adapters/googlechat.js +232 -0
  17. package/dist/adapters/googlechat.js.map +1 -0
  18. package/dist/adapters/matrix.d.ts +37 -0
  19. package/dist/adapters/matrix.d.ts.map +1 -0
  20. package/dist/adapters/matrix.js +262 -0
  21. package/dist/adapters/matrix.js.map +1 -0
  22. package/dist/adapters/signal.d.ts +32 -0
  23. package/dist/adapters/signal.d.ts.map +1 -0
  24. package/dist/adapters/signal.js +216 -0
  25. package/dist/adapters/signal.js.map +1 -0
  26. package/dist/adapters/slack.d.ts +29 -0
  27. package/dist/adapters/slack.d.ts.map +1 -0
  28. package/dist/adapters/slack.js +202 -0
  29. package/dist/adapters/slack.js.map +1 -0
  30. package/dist/adapters/teams.d.ts +66 -0
  31. package/dist/adapters/teams.d.ts.map +1 -0
  32. package/dist/adapters/teams.js +227 -0
  33. package/dist/adapters/teams.js.map +1 -0
  34. package/dist/adapters/telegram.d.ts +28 -0
  35. package/dist/adapters/telegram.d.ts.map +1 -0
  36. package/dist/adapters/telegram.js +170 -0
  37. package/dist/adapters/telegram.js.map +1 -0
  38. package/dist/adapters/twilio.d.ts +63 -0
  39. package/dist/adapters/twilio.d.ts.map +1 -0
  40. package/dist/adapters/twilio.js +193 -0
  41. package/dist/adapters/twilio.js.map +1 -0
  42. package/dist/adapters/whatsapp.d.ts +99 -0
  43. package/dist/adapters/whatsapp.d.ts.map +1 -0
  44. package/dist/adapters/whatsapp.js +218 -0
  45. package/dist/adapters/whatsapp.js.map +1 -0
  46. package/dist/adapters/zalo.d.ts +64 -0
  47. package/dist/adapters/zalo.d.ts.map +1 -0
  48. package/dist/adapters/zalo.js +216 -0
  49. package/dist/adapters/zalo.js.map +1 -0
  50. package/dist/index.d.ts +15 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +16 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/manager.d.ts +35 -0
  55. package/dist/manager.d.ts.map +1 -0
  56. package/dist/manager.js +127 -0
  57. package/dist/manager.js.map +1 -0
  58. package/dist/types.d.ts +71 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +32 -0
  63. package/src/adapters/bluebubbles.ts +294 -0
  64. package/src/adapters/discord.ts +253 -0
  65. package/src/adapters/email.ts +457 -0
  66. package/src/adapters/googlechat.ts +364 -0
  67. package/src/adapters/matrix.ts +376 -0
  68. package/src/adapters/signal.ts +313 -0
  69. package/src/adapters/slack.ts +252 -0
  70. package/src/adapters/teams.ts +320 -0
  71. package/src/adapters/telegram.ts +208 -0
  72. package/src/adapters/twilio.ts +256 -0
  73. package/src/adapters/whatsapp.ts +342 -0
  74. package/src/adapters/zalo.ts +319 -0
  75. package/src/index.ts +78 -0
  76. package/src/manager.ts +180 -0
  77. package/src/types.ts +84 -0
  78. package/tests/bluebubbles.test.ts +438 -0
  79. package/tests/email.test.ts +136 -0
  80. package/tests/googlechat.test.ts +439 -0
  81. package/tests/matrix.test.ts +564 -0
  82. package/tests/signal.test.ts +404 -0
  83. package/tests/slack.test.ts +343 -0
  84. package/tests/teams.test.ts +429 -0
  85. package/tests/twilio.test.ts +269 -0
  86. package/tests/whatsapp.test.ts +530 -0
  87. package/tests/zalo.test.ts +499 -0
  88. package/tsconfig.json +8 -0
  89. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,457 @@
1
+ import { audit } from '@auxiora/audit';
2
+ import type {
3
+ ChannelAdapter,
4
+ InboundMessage,
5
+ OutboundMessage,
6
+ SendResult,
7
+ } from '../types.js';
8
+
9
+ export interface EmailAdapterConfig {
10
+ imapHost: string;
11
+ imapPort: number;
12
+ smtpHost: string;
13
+ smtpPort: number;
14
+ email: string;
15
+ password: string;
16
+ pollInterval?: number;
17
+ allowedSenders?: string[];
18
+ tls?: boolean;
19
+ }
20
+
21
+ interface ImapMessage {
22
+ uid: string;
23
+ from: string;
24
+ fromName?: string;
25
+ to: string;
26
+ subject: string;
27
+ body: string;
28
+ date: Date;
29
+ messageId: string;
30
+ inReplyTo?: string;
31
+ references?: string[];
32
+ attachments?: Array<{
33
+ filename: string;
34
+ contentType: string;
35
+ size: number;
36
+ content?: Buffer;
37
+ }>;
38
+ }
39
+
40
+ const DEFAULT_POLL_INTERVAL = 30000; // 30 seconds
41
+
42
+ export class EmailAdapter implements ChannelAdapter {
43
+ readonly type = 'email' as const;
44
+ readonly name = 'Email';
45
+
46
+ private config: EmailAdapterConfig;
47
+ private messageHandler?: (message: InboundMessage) => Promise<void>;
48
+ private errorHandler?: (error: Error) => void;
49
+ private connected = false;
50
+ private pollTimer?: ReturnType<typeof setInterval>;
51
+ private seenUids: Set<string> = new Set();
52
+ private imapConnection?: ImapConnectionLike;
53
+ private smtpConnection?: SmtpConnectionLike;
54
+
55
+ constructor(config: EmailAdapterConfig) {
56
+ this.config = config;
57
+ }
58
+
59
+ async connect(): Promise<void> {
60
+ try {
61
+ // Connect IMAP
62
+ this.imapConnection = await this.connectImap();
63
+
64
+ // Connect SMTP
65
+ this.smtpConnection = await this.connectSmtp();
66
+
67
+ this.connected = true;
68
+
69
+ audit('channel.connected', {
70
+ channelType: 'email',
71
+ email: this.config.email,
72
+ });
73
+
74
+ // Start polling for new messages
75
+ this.startPolling();
76
+ } catch (error) {
77
+ throw new Error(`Failed to connect email: ${error instanceof Error ? error.message : error}`);
78
+ }
79
+ }
80
+
81
+ async disconnect(): Promise<void> {
82
+ if (this.pollTimer) {
83
+ clearInterval(this.pollTimer);
84
+ this.pollTimer = undefined;
85
+ }
86
+
87
+ try {
88
+ await this.imapConnection?.close();
89
+ } catch {
90
+ // Ignore close errors
91
+ }
92
+
93
+ try {
94
+ await this.smtpConnection?.close();
95
+ } catch {
96
+ // Ignore close errors
97
+ }
98
+
99
+ this.imapConnection = undefined;
100
+ this.smtpConnection = undefined;
101
+ this.connected = false;
102
+ audit('channel.disconnected', { channelType: 'email' });
103
+ }
104
+
105
+ isConnected(): boolean {
106
+ return this.connected;
107
+ }
108
+
109
+ private async connectImap(): Promise<ImapConnectionLike> {
110
+ // Use native TCP connection via Node.js net/tls modules
111
+ const net = await import('node:net');
112
+ const tls = await import('node:tls');
113
+
114
+ const useTls = this.config.tls !== false;
115
+ const socket = useTls
116
+ ? tls.connect({
117
+ host: this.config.imapHost,
118
+ port: this.config.imapPort,
119
+ rejectUnauthorized: true,
120
+ })
121
+ : net.connect({
122
+ host: this.config.imapHost,
123
+ port: this.config.imapPort,
124
+ });
125
+
126
+ return new Promise((resolve, reject) => {
127
+ const timeout = setTimeout(() => {
128
+ socket.destroy();
129
+ reject(new Error('IMAP connection timeout'));
130
+ }, 10000);
131
+
132
+ socket.once('error', (err: Error) => {
133
+ clearTimeout(timeout);
134
+ reject(err);
135
+ });
136
+
137
+ const onReady = () => {
138
+ clearTimeout(timeout);
139
+ resolve({
140
+ socket,
141
+ close: async () => { socket.destroy(); },
142
+ sendCommand: async (cmd: string) => {
143
+ return new Promise<string>((res, rej) => {
144
+ let data = '';
145
+ const onData = (chunk: Buffer) => {
146
+ data += chunk.toString();
147
+ if (data.includes('\r\n')) {
148
+ socket.removeListener('data', onData);
149
+ res(data);
150
+ }
151
+ };
152
+ socket.on('data', onData);
153
+ socket.write(`${cmd}\r\n`, (err?: Error | null) => {
154
+ if (err) rej(err);
155
+ });
156
+ });
157
+ },
158
+ });
159
+ };
160
+
161
+ if (useTls) {
162
+ (socket as import('node:tls').TLSSocket).once('secureConnect', onReady);
163
+ } else {
164
+ socket.once('connect', onReady);
165
+ }
166
+ });
167
+ }
168
+
169
+ private async connectSmtp(): Promise<SmtpConnectionLike> {
170
+ const net = await import('node:net');
171
+ const tls = await import('node:tls');
172
+
173
+ const useTls = this.config.tls !== false;
174
+ const socket = useTls
175
+ ? tls.connect({
176
+ host: this.config.smtpHost,
177
+ port: this.config.smtpPort,
178
+ rejectUnauthorized: true,
179
+ })
180
+ : net.connect({
181
+ host: this.config.smtpHost,
182
+ port: this.config.smtpPort,
183
+ });
184
+
185
+ return new Promise((resolve, reject) => {
186
+ const timeout = setTimeout(() => {
187
+ socket.destroy();
188
+ reject(new Error('SMTP connection timeout'));
189
+ }, 10000);
190
+
191
+ socket.once('error', (err: Error) => {
192
+ clearTimeout(timeout);
193
+ reject(err);
194
+ });
195
+
196
+ const onReady = () => {
197
+ clearTimeout(timeout);
198
+ resolve({
199
+ socket,
200
+ close: async () => { socket.destroy(); },
201
+ sendCommand: async (cmd: string) => {
202
+ return new Promise<string>((res, rej) => {
203
+ let data = '';
204
+ const onData = (chunk: Buffer) => {
205
+ data += chunk.toString();
206
+ if (data.includes('\r\n')) {
207
+ socket.removeListener('data', onData);
208
+ res(data);
209
+ }
210
+ };
211
+ socket.on('data', onData);
212
+ socket.write(`${cmd}\r\n`, (err?: Error | null) => {
213
+ if (err) rej(err);
214
+ });
215
+ });
216
+ },
217
+ });
218
+ };
219
+
220
+ if (useTls) {
221
+ (socket as import('node:tls').TLSSocket).once('secureConnect', onReady);
222
+ } else {
223
+ socket.once('connect', onReady);
224
+ }
225
+ });
226
+ }
227
+
228
+ private startPolling(): void {
229
+ const interval = this.config.pollInterval || DEFAULT_POLL_INTERVAL;
230
+ this.pollTimer = setInterval(() => {
231
+ void this.pollInbox();
232
+ }, interval);
233
+
234
+ // Initial poll
235
+ void this.pollInbox();
236
+ }
237
+
238
+ private async pollInbox(): Promise<void> {
239
+ if (!this.connected || !this.imapConnection) return;
240
+
241
+ try {
242
+ // Fetch messages using IMAP commands
243
+ const messages = await this.fetchNewMessages();
244
+
245
+ for (const msg of messages) {
246
+ if (this.seenUids.has(msg.uid)) continue;
247
+ this.seenUids.add(msg.uid);
248
+
249
+ // Check allowed senders whitelist
250
+ if (
251
+ this.config.allowedSenders?.length &&
252
+ !this.config.allowedSenders.some((s) =>
253
+ msg.from.toLowerCase().includes(s.toLowerCase())
254
+ )
255
+ ) {
256
+ continue;
257
+ }
258
+
259
+ const inbound = this.toInboundMessage(msg);
260
+
261
+ audit('message.received', {
262
+ channelType: 'email',
263
+ senderId: inbound.senderId,
264
+ channelId: inbound.channelId,
265
+ });
266
+
267
+ if (this.messageHandler) {
268
+ try {
269
+ await this.messageHandler(inbound);
270
+ } catch (error) {
271
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
272
+ }
273
+ }
274
+ }
275
+ } catch (error) {
276
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
277
+ }
278
+ }
279
+
280
+ private async fetchNewMessages(): Promise<ImapMessage[]> {
281
+ // Simplified IMAP fetch - in production you'd use a full IMAP client
282
+ // This sends raw IMAP commands over the socket connection
283
+ try {
284
+ await this.imapConnection!.sendCommand(`A001 LOGIN "${this.config.email}" "${this.config.password}"`);
285
+ await this.imapConnection!.sendCommand('A002 SELECT INBOX');
286
+ const searchResult = await this.imapConnection!.sendCommand('A003 SEARCH UNSEEN');
287
+
288
+ // Parse UIDs from search result
289
+ const match = searchResult.match(/\* SEARCH (.+)/);
290
+ if (!match) return [];
291
+
292
+ const uids = match[1].trim().split(/\s+/).filter(Boolean);
293
+ const messages: ImapMessage[] = [];
294
+
295
+ for (const uid of uids) {
296
+ try {
297
+ const fetchResult = await this.imapConnection!.sendCommand(
298
+ `A004 FETCH ${uid} (BODY[HEADER] BODY[TEXT])`
299
+ );
300
+
301
+ const msg = this.parseImapMessage(uid, fetchResult);
302
+ if (msg) messages.push(msg);
303
+ } catch {
304
+ // Skip individual message errors
305
+ }
306
+ }
307
+
308
+ return messages;
309
+ } catch {
310
+ return [];
311
+ }
312
+ }
313
+
314
+ private parseImapMessage(uid: string, raw: string): ImapMessage | null {
315
+ // Parse headers and body from IMAP FETCH response
316
+ const fromMatch = raw.match(/From:\s*(.+)/i);
317
+ const toMatch = raw.match(/To:\s*(.+)/i);
318
+ const subjectMatch = raw.match(/Subject:\s*(.+)/i);
319
+ const dateMatch = raw.match(/Date:\s*(.+)/i);
320
+ const messageIdMatch = raw.match(/Message-ID:\s*<(.+?)>/i);
321
+ const inReplyToMatch = raw.match(/In-Reply-To:\s*<(.+?)>/i);
322
+ const referencesMatch = raw.match(/References:\s*(.+)/i);
323
+
324
+ if (!fromMatch) return null;
325
+
326
+ // Extract the body (text after headers)
327
+ const headerBodySplit = raw.indexOf('\r\n\r\n');
328
+ const body = headerBodySplit > -1 ? raw.slice(headerBodySplit + 4).trim() : '';
329
+
330
+ // Parse from name and address
331
+ const fromFull = fromMatch[1].trim();
332
+ const fromNameMatch = fromFull.match(/^"?(.+?)"?\s*<(.+?)>$/);
333
+ const from = fromNameMatch ? fromNameMatch[2] : fromFull;
334
+ const fromName = fromNameMatch ? fromNameMatch[1] : undefined;
335
+
336
+ // Parse references
337
+ const references = referencesMatch
338
+ ? referencesMatch[1].match(/<(.+?)>/g)?.map((r) => r.slice(1, -1))
339
+ : undefined;
340
+
341
+ return {
342
+ uid,
343
+ from,
344
+ fromName,
345
+ to: toMatch?.[1].trim() || '',
346
+ subject: subjectMatch?.[1].trim() || '(no subject)',
347
+ body,
348
+ date: dateMatch ? new Date(dateMatch[1].trim()) : new Date(),
349
+ messageId: messageIdMatch?.[1] || `${uid}@unknown`,
350
+ inReplyTo: inReplyToMatch?.[1],
351
+ references,
352
+ };
353
+ }
354
+
355
+ private toInboundMessage(msg: ImapMessage): InboundMessage {
356
+ // Use the email thread (via In-Reply-To/References) as the channel
357
+ const threadId = msg.references?.[0] || msg.inReplyTo || msg.messageId;
358
+
359
+ return {
360
+ id: msg.messageId,
361
+ channelType: 'email',
362
+ channelId: threadId,
363
+ senderId: msg.from,
364
+ senderName: msg.fromName,
365
+ content: `Subject: ${msg.subject}\n\n${msg.body}`,
366
+ timestamp: msg.date.getTime(),
367
+ replyToId: msg.inReplyTo,
368
+ attachments: msg.attachments?.map((a) => ({
369
+ type: a.contentType.startsWith('image/')
370
+ ? 'image' as const
371
+ : 'file' as const,
372
+ mimeType: a.contentType,
373
+ filename: a.filename,
374
+ size: a.size,
375
+ data: a.content,
376
+ })),
377
+ raw: msg,
378
+ };
379
+ }
380
+
381
+ async send(channelId: string, message: OutboundMessage): Promise<SendResult> {
382
+ try {
383
+ if (!this.smtpConnection) {
384
+ return { success: false, error: 'SMTP not connected' };
385
+ }
386
+
387
+ // Parse the recipient from the channelId (thread message-id) or replyToId
388
+ // In email adapter, channelId is the thread message-id
389
+ // The actual recipient is derived from the original sender
390
+ const messageId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@auxiora>`;
391
+
392
+ // Build email
393
+ const headers = [
394
+ `From: ${this.config.email}`,
395
+ `To: ${channelId}`,
396
+ `Subject: Re: Auxiora`,
397
+ `Message-ID: ${messageId}`,
398
+ `Date: ${new Date().toUTCString()}`,
399
+ 'MIME-Version: 1.0',
400
+ 'Content-Type: text/plain; charset=utf-8',
401
+ ];
402
+
403
+ if (message.replyToId) {
404
+ headers.push(`In-Reply-To: <${message.replyToId}>`);
405
+ headers.push(`References: <${message.replyToId}>`);
406
+ }
407
+
408
+ const emailContent = headers.join('\r\n') + '\r\n\r\n' + message.content;
409
+
410
+ // Send via SMTP
411
+ await this.smtpConnection.sendCommand(`EHLO auxiora`);
412
+ await this.smtpConnection.sendCommand(
413
+ `AUTH PLAIN ${Buffer.from(`\0${this.config.email}\0${this.config.password}`).toString('base64')}`
414
+ );
415
+ await this.smtpConnection.sendCommand(`MAIL FROM:<${this.config.email}>`);
416
+ await this.smtpConnection.sendCommand(`RCPT TO:<${channelId}>`);
417
+ await this.smtpConnection.sendCommand('DATA');
418
+ await this.smtpConnection.sendCommand(`${emailContent}\r\n.`);
419
+
420
+ audit('message.sent', {
421
+ channelType: 'email',
422
+ channelId,
423
+ messageId,
424
+ });
425
+
426
+ return { success: true, messageId };
427
+ } catch (error) {
428
+ const errorMessage = error instanceof Error ? error.message : String(error);
429
+ audit('channel.error', {
430
+ channelType: 'email',
431
+ action: 'send',
432
+ error: errorMessage,
433
+ });
434
+ return { success: false, error: errorMessage };
435
+ }
436
+ }
437
+
438
+ onMessage(handler: (message: InboundMessage) => Promise<void>): void {
439
+ this.messageHandler = handler;
440
+ }
441
+
442
+ onError(handler: (error: Error) => void): void {
443
+ this.errorHandler = handler;
444
+ }
445
+ }
446
+
447
+ interface ImapConnectionLike {
448
+ socket: unknown;
449
+ close: () => Promise<void>;
450
+ sendCommand: (cmd: string) => Promise<string>;
451
+ }
452
+
453
+ interface SmtpConnectionLike {
454
+ socket: unknown;
455
+ close: () => Promise<void>;
456
+ sendCommand: (cmd: string) => Promise<string>;
457
+ }