@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.
- package/LICENSE +191 -0
- package/dist/adapters/bluebubbles.d.ts +63 -0
- package/dist/adapters/bluebubbles.d.ts.map +1 -0
- package/dist/adapters/bluebubbles.js +197 -0
- package/dist/adapters/bluebubbles.js.map +1 -0
- package/dist/adapters/discord.d.ts +27 -0
- package/dist/adapters/discord.d.ts.map +1 -0
- package/dist/adapters/discord.js +202 -0
- package/dist/adapters/discord.js.map +1 -0
- package/dist/adapters/email.d.ts +39 -0
- package/dist/adapters/email.d.ts.map +1 -0
- package/dist/adapters/email.js +359 -0
- package/dist/adapters/email.js.map +1 -0
- package/dist/adapters/googlechat.d.ts +77 -0
- package/dist/adapters/googlechat.d.ts.map +1 -0
- package/dist/adapters/googlechat.js +232 -0
- package/dist/adapters/googlechat.js.map +1 -0
- package/dist/adapters/matrix.d.ts +37 -0
- package/dist/adapters/matrix.d.ts.map +1 -0
- package/dist/adapters/matrix.js +262 -0
- package/dist/adapters/matrix.js.map +1 -0
- package/dist/adapters/signal.d.ts +32 -0
- package/dist/adapters/signal.d.ts.map +1 -0
- package/dist/adapters/signal.js +216 -0
- package/dist/adapters/signal.js.map +1 -0
- package/dist/adapters/slack.d.ts +29 -0
- package/dist/adapters/slack.d.ts.map +1 -0
- package/dist/adapters/slack.js +202 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/adapters/teams.d.ts +66 -0
- package/dist/adapters/teams.d.ts.map +1 -0
- package/dist/adapters/teams.js +227 -0
- package/dist/adapters/teams.js.map +1 -0
- package/dist/adapters/telegram.d.ts +28 -0
- package/dist/adapters/telegram.d.ts.map +1 -0
- package/dist/adapters/telegram.js +170 -0
- package/dist/adapters/telegram.js.map +1 -0
- package/dist/adapters/twilio.d.ts +63 -0
- package/dist/adapters/twilio.d.ts.map +1 -0
- package/dist/adapters/twilio.js +193 -0
- package/dist/adapters/twilio.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +99 -0
- package/dist/adapters/whatsapp.d.ts.map +1 -0
- package/dist/adapters/whatsapp.js +218 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/adapters/zalo.d.ts +64 -0
- package/dist/adapters/zalo.d.ts.map +1 -0
- package/dist/adapters/zalo.js +216 -0
- package/dist/adapters/zalo.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +35 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +127 -0
- package/dist/manager.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
- package/src/adapters/bluebubbles.ts +294 -0
- package/src/adapters/discord.ts +253 -0
- package/src/adapters/email.ts +457 -0
- package/src/adapters/googlechat.ts +364 -0
- package/src/adapters/matrix.ts +376 -0
- package/src/adapters/signal.ts +313 -0
- package/src/adapters/slack.ts +252 -0
- package/src/adapters/teams.ts +320 -0
- package/src/adapters/telegram.ts +208 -0
- package/src/adapters/twilio.ts +256 -0
- package/src/adapters/whatsapp.ts +342 -0
- package/src/adapters/zalo.ts +319 -0
- package/src/index.ts +78 -0
- package/src/manager.ts +180 -0
- package/src/types.ts +84 -0
- package/tests/bluebubbles.test.ts +438 -0
- package/tests/email.test.ts +136 -0
- package/tests/googlechat.test.ts +439 -0
- package/tests/matrix.test.ts +564 -0
- package/tests/signal.test.ts +404 -0
- package/tests/slack.test.ts +343 -0
- package/tests/teams.test.ts +429 -0
- package/tests/twilio.test.ts +269 -0
- package/tests/whatsapp.test.ts +530 -0
- package/tests/zalo.test.ts +499 -0
- package/tsconfig.json +8 -0
- 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
|
+
}
|