@inbrx/cli 0.0.1-alpha.3 → 0.0.1-alpha.4
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/README.md +78 -0
- package/dist/cli.js +7 -0
- package/dist/config.js +39 -0
- package/dist/events/mailbox-events.js +16 -0
- package/dist/http/server.js +75 -4
- package/dist/index.js +23 -6
- package/dist/smtp/server.js +51 -4
- package/dist/store/file-store.js +3 -3
- package/dist/store/memory-store.js +3 -3
- package/package.json +5 -3
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @inbrx/cli
|
|
2
|
+
|
|
3
|
+
CLI for capturing local SMTP mail with inbrx. Start it, point your application at the local SMTP endpoint, and inspect captured emails in the browser.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @inbrx/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Defaults:
|
|
12
|
+
|
|
13
|
+
- SMTP server: `127.0.0.1:2525`
|
|
14
|
+
- Web UI: `http://127.0.0.1:3000`
|
|
15
|
+
- Storage: file-backed, using the standard user data directory for your operating system
|
|
16
|
+
|
|
17
|
+
Use memory-only storage for throwaway sessions:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @inbrx/cli --storage memory
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Options
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
inbrx start \
|
|
27
|
+
--smtp-host 127.0.0.1 \
|
|
28
|
+
--smtp-port 2525 \
|
|
29
|
+
--smtp-starttls \
|
|
30
|
+
--smtp-tls-key ./smtp.key \
|
|
31
|
+
--smtp-tls-cert ./smtp.crt \
|
|
32
|
+
--http-host 127.0.0.1 \
|
|
33
|
+
--http-port 3000 \
|
|
34
|
+
--max-messages 500 \
|
|
35
|
+
--storage file
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Environment variables:
|
|
39
|
+
|
|
40
|
+
- `SMTP_TEST_SMTP_HOST`
|
|
41
|
+
- `SMTP_TEST_SMTP_PORT`
|
|
42
|
+
- `INBRX_SMTP_STARTTLS=true|false`
|
|
43
|
+
- `INBRX_SMTP_TLS_KEY`
|
|
44
|
+
- `INBRX_SMTP_TLS_CERT`
|
|
45
|
+
- `SMTP_TEST_HTTP_HOST`
|
|
46
|
+
- `SMTP_TEST_HTTP_PORT`
|
|
47
|
+
- `SMTP_TEST_MAX_MESSAGES`
|
|
48
|
+
- `INBRX_STORAGE=file|memory`
|
|
49
|
+
|
|
50
|
+
## STARTTLS
|
|
51
|
+
|
|
52
|
+
Enable STARTTLS with an automatically generated in-memory self-signed certificate:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
inbrx start --smtp-starttls
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use your own certificate by providing both paths. Providing both paths also enables STARTTLS:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
inbrx start --smtp-tls-key ./smtp.key --smtp-tls-cert ./smtp.crt
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Self-signed certificates usually require test clients to disable certificate verification:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
swaks --to recipient@example.com \
|
|
68
|
+
--from sender@example.com \
|
|
69
|
+
--server 127.0.0.1 \
|
|
70
|
+
--port 2525 \
|
|
71
|
+
--tls \
|
|
72
|
+
--tls-verify false \
|
|
73
|
+
--auth LOGIN \
|
|
74
|
+
--auth-user sender@example.com \
|
|
75
|
+
--auth-password 'your_password' \
|
|
76
|
+
--header "Subject: test email" \
|
|
77
|
+
--body "Hello from SMTP"
|
|
78
|
+
```
|
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,9 @@ export function createCliProgram() {
|
|
|
19
19
|
.description('Start the SMTP server and HTTP API/UI.')
|
|
20
20
|
.option('--smtp-host <host>', `SMTP bind host. Default: ${defaults.smtpHost}`)
|
|
21
21
|
.option('--smtp-port <port>', `SMTP port. Default: ${defaults.smtpPort}`)
|
|
22
|
+
.option('--smtp-starttls', 'Advertise and support STARTTLS. Generates a temporary self-signed certificate unless TLS paths are provided.')
|
|
23
|
+
.option('--smtp-tls-key <path>', 'TLS private key path for STARTTLS. Enables STARTTLS when paired with --smtp-tls-cert.')
|
|
24
|
+
.option('--smtp-tls-cert <path>', 'TLS certificate path for STARTTLS. Enables STARTTLS when paired with --smtp-tls-key.')
|
|
22
25
|
.option('--http-host <host>', `HTTP bind host. Default: ${defaults.httpHost}`)
|
|
23
26
|
.option('--http-port <port>', `HTTP port. Default: ${defaults.httpPort}`)
|
|
24
27
|
.option('--max-messages <count>', `Maximum retained messages. Default: ${defaults.maxMessages}`)
|
|
@@ -28,6 +31,9 @@ export function createCliProgram() {
|
|
|
28
31
|
Environment:
|
|
29
32
|
SMTP_TEST_SMTP_HOST
|
|
30
33
|
SMTP_TEST_SMTP_PORT
|
|
34
|
+
INBRX_SMTP_STARTTLS
|
|
35
|
+
INBRX_SMTP_TLS_KEY
|
|
36
|
+
INBRX_SMTP_TLS_CERT
|
|
31
37
|
SMTP_TEST_HTTP_HOST
|
|
32
38
|
SMTP_TEST_HTTP_PORT
|
|
33
39
|
SMTP_TEST_MAX_MESSAGES
|
|
@@ -42,6 +48,7 @@ async function startServer(options) {
|
|
|
42
48
|
const app = await startApp(config);
|
|
43
49
|
console.log('SMTP test server ready');
|
|
44
50
|
console.log(`SMTP: smtp://${config.smtpHost}:${config.smtpPort}`);
|
|
51
|
+
console.log(`SMTP STARTTLS: ${config.smtpStartTls ? 'enabled' : 'disabled'}`);
|
|
45
52
|
console.log(`Web UI: http://${config.httpHost}:${config.httpPort}`);
|
|
46
53
|
console.log(`Storage: ${config.storage}${config.storage === 'file' ? ` (${getDefaultDataDir()})` : ''}`);
|
|
47
54
|
console.log('Press Ctrl+C to stop.');
|
package/dist/config.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
const DEFAULTS = {
|
|
2
2
|
smtpHost: '127.0.0.1',
|
|
3
3
|
smtpPort: 2525,
|
|
4
|
+
smtpStartTls: false,
|
|
5
|
+
smtpTlsKeyPath: null,
|
|
6
|
+
smtpTlsCertPath: null,
|
|
4
7
|
httpHost: '127.0.0.1',
|
|
5
8
|
httpPort: 3000,
|
|
6
9
|
maxMessages: 500,
|
|
7
10
|
storage: 'file'
|
|
8
11
|
};
|
|
9
12
|
export function loadConfig(overrides = {}) {
|
|
13
|
+
const smtpTlsKeyPath = readOptionalString(overrides.smtpTlsKeyPath, process.env.INBRX_SMTP_TLS_KEY, DEFAULTS.smtpTlsKeyPath);
|
|
14
|
+
const smtpTlsCertPath = readOptionalString(overrides.smtpTlsCertPath, process.env.INBRX_SMTP_TLS_CERT, DEFAULTS.smtpTlsCertPath);
|
|
10
15
|
const config = {
|
|
11
16
|
smtpHost: readString(overrides.smtpHost, process.env.SMTP_TEST_SMTP_HOST, DEFAULTS.smtpHost),
|
|
12
17
|
smtpPort: readNumber(overrides.smtpPort, process.env.SMTP_TEST_SMTP_PORT, DEFAULTS.smtpPort),
|
|
18
|
+
smtpStartTls: readBoolean(overrides.smtpStartTls ?? overrides.smtpStarttls, process.env.INBRX_SMTP_STARTTLS, DEFAULTS.smtpStartTls) ||
|
|
19
|
+
Boolean(smtpTlsKeyPath) ||
|
|
20
|
+
Boolean(smtpTlsCertPath),
|
|
21
|
+
smtpTlsKeyPath,
|
|
22
|
+
smtpTlsCertPath,
|
|
13
23
|
httpHost: readString(overrides.httpHost, process.env.SMTP_TEST_HTTP_HOST, DEFAULTS.httpHost),
|
|
14
24
|
httpPort: readNumber(overrides.httpPort, process.env.SMTP_TEST_HTTP_PORT, DEFAULTS.httpPort),
|
|
15
25
|
maxMessages: readNumber(overrides.maxMessages, process.env.SMTP_TEST_MAX_MESSAGES, DEFAULTS.maxMessages),
|
|
@@ -17,6 +27,9 @@ export function loadConfig(overrides = {}) {
|
|
|
17
27
|
};
|
|
18
28
|
assertPort(config.smtpPort, 'SMTP port');
|
|
19
29
|
assertPort(config.httpPort, 'HTTP port');
|
|
30
|
+
if (Boolean(config.smtpTlsKeyPath) !== Boolean(config.smtpTlsCertPath)) {
|
|
31
|
+
throw new Error('SMTP TLS key and certificate paths must be provided together.');
|
|
32
|
+
}
|
|
20
33
|
if (!Number.isInteger(config.maxMessages) || config.maxMessages < 1) {
|
|
21
34
|
throw new Error('Max messages must be a positive integer.');
|
|
22
35
|
}
|
|
@@ -34,6 +47,15 @@ function readString(override, envValue, fallback) {
|
|
|
34
47
|
}
|
|
35
48
|
return fallback;
|
|
36
49
|
}
|
|
50
|
+
function readOptionalString(override, envValue, fallback) {
|
|
51
|
+
if (override !== undefined && override !== null && override !== '') {
|
|
52
|
+
return String(override);
|
|
53
|
+
}
|
|
54
|
+
if (envValue !== undefined && envValue !== '') {
|
|
55
|
+
return envValue;
|
|
56
|
+
}
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
37
59
|
function readNumber(override, envValue, fallback) {
|
|
38
60
|
const value = override ?? envValue;
|
|
39
61
|
if (value === undefined || value === '') {
|
|
@@ -45,6 +67,23 @@ function readNumber(override, envValue, fallback) {
|
|
|
45
67
|
}
|
|
46
68
|
return parsed;
|
|
47
69
|
}
|
|
70
|
+
function readBoolean(override, envValue, fallback) {
|
|
71
|
+
const value = override ?? envValue;
|
|
72
|
+
if (value === undefined || value === null || value === '') {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'boolean') {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
const normalized = String(value).toLowerCase();
|
|
79
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Expected a boolean, received "${value}".`);
|
|
86
|
+
}
|
|
48
87
|
function assertPort(value, label) {
|
|
49
88
|
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
50
89
|
throw new Error(`${label} must be an integer between 1 and 65535.`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createMailboxEvents() {
|
|
2
|
+
const listeners = new Set();
|
|
3
|
+
return {
|
|
4
|
+
emit(event) {
|
|
5
|
+
for (const listener of listeners) {
|
|
6
|
+
listener(event);
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
subscribe(listener) {
|
|
10
|
+
listeners.add(listener);
|
|
11
|
+
return () => {
|
|
12
|
+
listeners.delete(listener);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/http/server.js
CHANGED
|
@@ -5,10 +5,11 @@ import { Readable } from 'node:stream';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { serve } from '@hono/node-server';
|
|
7
7
|
import { Hono } from 'hono';
|
|
8
|
+
import { createMailboxEvents } from '../events/mailbox-events.js';
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const WEB_ROOT = path.resolve(__dirname, '../../..', 'web/dist');
|
|
10
|
-
export function createHttpServer({ store, attachmentStore }) {
|
|
11
|
-
const app = createHttpApp({ store, attachmentStore });
|
|
11
|
+
export function createHttpServer({ store, attachmentStore, events = createMailboxEvents() }) {
|
|
12
|
+
const app = createHttpApp({ store, attachmentStore, events });
|
|
12
13
|
let server = null;
|
|
13
14
|
return {
|
|
14
15
|
listen(port, host) {
|
|
@@ -43,18 +44,20 @@ export function createHttpServer({ store, attachmentStore }) {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
|
-
export function createHttpApp({ store, attachmentStore }) {
|
|
47
|
+
export function createHttpApp({ store, attachmentStore, events = createMailboxEvents() }) {
|
|
47
48
|
const app = new Hono();
|
|
48
49
|
app.onError((error, c) => {
|
|
49
50
|
console.error(error);
|
|
50
51
|
return c.json({ error: 'Internal server error' }, 500);
|
|
51
52
|
});
|
|
52
53
|
app.get('/api/health', (c) => c.json({ status: 'ok' }));
|
|
54
|
+
app.get('/api/events', (c) => sseResponse(events, c.req.raw.signal));
|
|
53
55
|
app.get('/api/messages', async (c) => c.json({
|
|
54
56
|
messages: (await store.list()).map(toMessageSummary)
|
|
55
57
|
}));
|
|
56
58
|
app.delete('/api/messages', async (c) => {
|
|
57
59
|
const deleted = await store.clear();
|
|
60
|
+
events.emit({ type: 'messages.cleared', deleted });
|
|
58
61
|
return c.json({ deleted });
|
|
59
62
|
});
|
|
60
63
|
app.get('/api/messages/:id', async (c) => {
|
|
@@ -84,12 +87,80 @@ export function createHttpApp({ store, attachmentStore }) {
|
|
|
84
87
|
});
|
|
85
88
|
});
|
|
86
89
|
app.delete('/api/messages/:id', async (c) => {
|
|
87
|
-
const
|
|
90
|
+
const messageId = c.req.param('id');
|
|
91
|
+
const deleted = await store.delete(messageId);
|
|
92
|
+
if (deleted) {
|
|
93
|
+
events.emit({ type: 'message.deleted', id: messageId });
|
|
94
|
+
}
|
|
88
95
|
return c.json({ deleted }, deleted ? 200 : 404);
|
|
89
96
|
});
|
|
90
97
|
app.all('*', (c) => serveStatic(new URL(c.req.url).pathname));
|
|
91
98
|
return app;
|
|
92
99
|
}
|
|
100
|
+
function sseResponse(events, signal) {
|
|
101
|
+
const encoder = new TextEncoder();
|
|
102
|
+
let unsubscribe = null;
|
|
103
|
+
let keepalive = null;
|
|
104
|
+
let closed = false;
|
|
105
|
+
const stream = new ReadableStream({
|
|
106
|
+
start(controller) {
|
|
107
|
+
const send = (event, data) => {
|
|
108
|
+
if (closed) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
112
|
+
};
|
|
113
|
+
const cleanup = () => {
|
|
114
|
+
if (closed) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
closed = true;
|
|
118
|
+
unsubscribe?.();
|
|
119
|
+
unsubscribe = null;
|
|
120
|
+
if (keepalive) {
|
|
121
|
+
clearInterval(keepalive);
|
|
122
|
+
keepalive = null;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
send('ready', { version: 1 });
|
|
126
|
+
unsubscribe = events.subscribe((event) => {
|
|
127
|
+
send(event.type, eventData(event));
|
|
128
|
+
});
|
|
129
|
+
keepalive = setInterval(() => {
|
|
130
|
+
if (!closed) {
|
|
131
|
+
controller.enqueue(encoder.encode(': keepalive\n\n'));
|
|
132
|
+
}
|
|
133
|
+
}, 30000);
|
|
134
|
+
signal.addEventListener('abort', cleanup, { once: true });
|
|
135
|
+
},
|
|
136
|
+
cancel() {
|
|
137
|
+
closed = true;
|
|
138
|
+
unsubscribe?.();
|
|
139
|
+
unsubscribe = null;
|
|
140
|
+
if (keepalive) {
|
|
141
|
+
clearInterval(keepalive);
|
|
142
|
+
keepalive = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return new Response(stream, {
|
|
147
|
+
headers: {
|
|
148
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
149
|
+
'cache-control': 'no-cache, no-transform',
|
|
150
|
+
connection: 'keep-alive'
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function eventData(event) {
|
|
155
|
+
switch (event.type) {
|
|
156
|
+
case 'message.created':
|
|
157
|
+
return { id: event.id, receivedAt: event.receivedAt };
|
|
158
|
+
case 'message.deleted':
|
|
159
|
+
return { id: event.id };
|
|
160
|
+
case 'messages.cleared':
|
|
161
|
+
return { deleted: event.deleted };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
93
164
|
function toMessageSummary(message) {
|
|
94
165
|
return {
|
|
95
166
|
id: message.id,
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMailboxEvents } from './events/mailbox-events.js';
|
|
1
2
|
import { createHttpServer } from './http/server.js';
|
|
2
3
|
import { createFileAttachmentStore, createMemoryAttachmentStore } from './store/attachment-store.js';
|
|
3
4
|
import { getDefaultDataDir } from './store/data-dir.js';
|
|
@@ -5,9 +6,17 @@ import { createFileMessageStore } from './store/file-store.js';
|
|
|
5
6
|
import { createMemoryStore } from './store/memory-store.js';
|
|
6
7
|
import { createSmtpServer } from './smtp/server.js';
|
|
7
8
|
export async function startApp(config) {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const events = createMailboxEvents();
|
|
10
|
+
const { store, attachmentStore } = createStores(config, events);
|
|
11
|
+
const smtpServer = await createSmtpServer({
|
|
12
|
+
store,
|
|
13
|
+
attachmentStore,
|
|
14
|
+
events,
|
|
15
|
+
startTls: config.smtpStartTls,
|
|
16
|
+
tlsKeyPath: config.smtpTlsKeyPath,
|
|
17
|
+
tlsCertPath: config.smtpTlsCertPath
|
|
18
|
+
});
|
|
19
|
+
const httpServer = createHttpServer({ store, attachmentStore, events });
|
|
11
20
|
await Promise.all([
|
|
12
21
|
smtpServer.listen(config.smtpPort, config.smtpHost),
|
|
13
22
|
httpServer.listen(config.httpPort, config.httpHost)
|
|
@@ -19,14 +28,14 @@ export async function startApp(config) {
|
|
|
19
28
|
}
|
|
20
29
|
};
|
|
21
30
|
}
|
|
22
|
-
function createStores(config) {
|
|
31
|
+
function createStores(config, events) {
|
|
23
32
|
if (config.storage === 'memory') {
|
|
24
33
|
const attachmentStore = createMemoryAttachmentStore();
|
|
25
34
|
return {
|
|
26
35
|
attachmentStore,
|
|
27
36
|
store: createMemoryStore({
|
|
28
37
|
maxMessages: config.maxMessages,
|
|
29
|
-
onDelete: (
|
|
38
|
+
onDelete: createDeleteHandler(attachmentStore, events)
|
|
30
39
|
})
|
|
31
40
|
};
|
|
32
41
|
}
|
|
@@ -37,7 +46,15 @@ function createStores(config) {
|
|
|
37
46
|
store: createFileMessageStore({
|
|
38
47
|
rootDir: dataDir,
|
|
39
48
|
maxMessages: config.maxMessages,
|
|
40
|
-
onDelete: (
|
|
49
|
+
onDelete: createDeleteHandler(attachmentStore, events)
|
|
41
50
|
})
|
|
42
51
|
};
|
|
43
52
|
}
|
|
53
|
+
function createDeleteHandler(attachmentStore, events) {
|
|
54
|
+
return async (messageId, reason) => {
|
|
55
|
+
await attachmentStore.deleteForMessage(messageId);
|
|
56
|
+
if (reason === 'evicted') {
|
|
57
|
+
events.emit({ type: 'message.deleted', id: messageId });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/smtp/server.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import { SMTPServer } from 'smtp-server';
|
|
3
|
+
import { generate } from 'selfsigned';
|
|
2
4
|
import { parseMessage } from '../mail/parser.js';
|
|
3
|
-
export function createSmtpServer({ store, attachmentStore }) {
|
|
5
|
+
export async function createSmtpServer({ store, attachmentStore, events, startTls = false, tlsKeyPath = null, tlsCertPath = null }) {
|
|
6
|
+
const tlsConfig = startTls ? await createTlsConfig({ keyPath: tlsKeyPath, certPath: tlsCertPath }) : null;
|
|
4
7
|
const server = new SMTPServer({
|
|
5
8
|
name: 'inbrx',
|
|
6
9
|
banner: 'inbrx ready',
|
|
10
|
+
...(tlsConfig ?? {}),
|
|
7
11
|
authOptional: true,
|
|
8
|
-
|
|
12
|
+
allowInsecureAuth: true,
|
|
13
|
+
disabledCommands: startTls ? [] : ['STARTTLS'],
|
|
9
14
|
hidePIPELINING: true,
|
|
10
15
|
logger: false,
|
|
16
|
+
onAuth(auth, _session, callback) {
|
|
17
|
+
callback(null, { user: auth.username || 'anonymous' });
|
|
18
|
+
},
|
|
11
19
|
onMailFrom(_address, _session, callback) {
|
|
12
20
|
callback();
|
|
13
21
|
},
|
|
@@ -16,7 +24,10 @@ export function createSmtpServer({ store, attachmentStore }) {
|
|
|
16
24
|
},
|
|
17
25
|
onData(stream, session, callback) {
|
|
18
26
|
void handleData(stream, session, store, attachmentStore)
|
|
19
|
-
.then((
|
|
27
|
+
.then((message) => {
|
|
28
|
+
events?.emit({ type: 'message.created', id: message.id, receivedAt: message.receivedAt });
|
|
29
|
+
callback(null, `OK captured as ${message.id}`);
|
|
30
|
+
})
|
|
20
31
|
.catch((error) => callback(error instanceof Error ? error : new Error(String(error))));
|
|
21
32
|
}
|
|
22
33
|
});
|
|
@@ -37,6 +48,42 @@ export function createSmtpServer({ store, attachmentStore }) {
|
|
|
37
48
|
}
|
|
38
49
|
};
|
|
39
50
|
}
|
|
51
|
+
async function createTlsConfig({ keyPath, certPath }) {
|
|
52
|
+
if (keyPath || certPath) {
|
|
53
|
+
if (!keyPath || !certPath) {
|
|
54
|
+
throw new Error('SMTP TLS key and certificate paths must be provided together.');
|
|
55
|
+
}
|
|
56
|
+
const [key, cert] = await Promise.all([readFile(keyPath), readFile(certPath)]);
|
|
57
|
+
return { key, cert };
|
|
58
|
+
}
|
|
59
|
+
const pems = await generate([{ name: 'commonName', value: 'localhost' }], {
|
|
60
|
+
keySize: 2048,
|
|
61
|
+
algorithm: 'sha256',
|
|
62
|
+
extensions: [
|
|
63
|
+
{
|
|
64
|
+
name: 'basicConstraints',
|
|
65
|
+
cA: false
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'keyUsage',
|
|
69
|
+
digitalSignature: true,
|
|
70
|
+
keyEncipherment: true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'subjectAltName',
|
|
74
|
+
altNames: [
|
|
75
|
+
{ type: 2, value: 'localhost' },
|
|
76
|
+
{ type: 7, ip: '127.0.0.1' },
|
|
77
|
+
{ type: 7, ip: '::1' }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
key: pems.private,
|
|
84
|
+
cert: pems.cert
|
|
85
|
+
};
|
|
86
|
+
}
|
|
40
87
|
async function handleData(stream, session, store, attachmentStore) {
|
|
41
88
|
const raw = await readStream(stream);
|
|
42
89
|
const smtp = toCapturedSmtpSession(session);
|
|
@@ -49,7 +96,7 @@ async function handleData(stream, session, store, attachmentStore) {
|
|
|
49
96
|
await attachmentStore.deleteForMessage(message.id);
|
|
50
97
|
throw error;
|
|
51
98
|
}
|
|
52
|
-
return message
|
|
99
|
+
return message;
|
|
53
100
|
}
|
|
54
101
|
function readStream(stream) {
|
|
55
102
|
return new Promise((resolve, reject) => {
|
package/dist/store/file-store.js
CHANGED
|
@@ -21,13 +21,13 @@ export function createFileMessageStore({ rootDir, maxMessages, onDelete }) {
|
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
23
|
await rm(messagePath(messagesDir, id), { force: true });
|
|
24
|
-
await onDelete?.(id);
|
|
24
|
+
await onDelete?.(id, 'deleted');
|
|
25
25
|
return true;
|
|
26
26
|
},
|
|
27
27
|
async clear() {
|
|
28
28
|
const messages = await listMessages(messagesDir);
|
|
29
29
|
await rm(messagesDir, { recursive: true, force: true });
|
|
30
|
-
await Promise.all(messages.map((message) => onDelete?.(message.id)));
|
|
30
|
+
await Promise.all(messages.map((message) => onDelete?.(message.id, 'cleared')));
|
|
31
31
|
return messages.length;
|
|
32
32
|
}
|
|
33
33
|
};
|
|
@@ -37,7 +37,7 @@ async function trimMessages({ messagesDir, maxMessages, onDelete }) {
|
|
|
37
37
|
const removed = messages.slice(maxMessages);
|
|
38
38
|
await Promise.all(removed.map(async (message) => {
|
|
39
39
|
await rm(messagePath(messagesDir, message.id), { force: true });
|
|
40
|
-
await onDelete?.(message.id);
|
|
40
|
+
await onDelete?.(message.id, 'evicted');
|
|
41
41
|
}));
|
|
42
42
|
}
|
|
43
43
|
async function listMessages(messagesDir) {
|
|
@@ -9,7 +9,7 @@ export function createMemoryStore({ maxMessages, onDelete }) {
|
|
|
9
9
|
const removedId = order.pop();
|
|
10
10
|
if (removedId) {
|
|
11
11
|
messages.delete(removedId);
|
|
12
|
-
await onDelete?.(removedId);
|
|
12
|
+
await onDelete?.(removedId, 'evicted');
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
return message;
|
|
@@ -27,7 +27,7 @@ export function createMemoryStore({ maxMessages, onDelete }) {
|
|
|
27
27
|
order.splice(index, 1);
|
|
28
28
|
}
|
|
29
29
|
if (existed) {
|
|
30
|
-
await onDelete?.(id);
|
|
30
|
+
await onDelete?.(id, 'deleted');
|
|
31
31
|
}
|
|
32
32
|
return existed;
|
|
33
33
|
},
|
|
@@ -36,7 +36,7 @@ export function createMemoryStore({ maxMessages, onDelete }) {
|
|
|
36
36
|
const ids = [...messages.keys()];
|
|
37
37
|
messages.clear();
|
|
38
38
|
order.length = 0;
|
|
39
|
-
await Promise.all(ids.map((id) => onDelete?.(id)));
|
|
39
|
+
await Promise.all(ids.map((id) => onDelete?.(id, 'cleared')));
|
|
40
40
|
return count;
|
|
41
41
|
}
|
|
42
42
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inbrx/cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"description": "CLI for capturing local SMTP mail with inbrx.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
|
-
"dist"
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"bin": {
|
|
15
16
|
"inbrx": "./bin/inbrx.js"
|
|
@@ -28,10 +29,11 @@
|
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@hono/node-server": "^2.0.8",
|
|
31
|
-
"@inbrx/web": "0.0.1-alpha.
|
|
32
|
+
"@inbrx/web": "0.0.1-alpha.4",
|
|
32
33
|
"commander": "^15.0.0",
|
|
33
34
|
"hono": "^4.12.27",
|
|
34
35
|
"mailparser": "^3.9.14",
|
|
36
|
+
"selfsigned": "^5.5.0",
|
|
35
37
|
"smtp-server": "^3.19.2"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|