@open-mercato/channel-smtp 0.0.0-stage → 0.10.1-develop.7749.1.ea3edf75e3
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/.turbo/turbo-build.log +2 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_smtp/__integration__/TC-CHANNEL-SMTP-001.spec.js +97 -0
- package/dist/modules/channel_smtp/__integration__/TC-CHANNEL-SMTP-001.spec.js.map +7 -0
- package/dist/modules/channel_smtp/__integration__/meta.js +10 -0
- package/dist/modules/channel_smtp/__integration__/meta.js.map +7 -0
- package/dist/modules/channel_smtp/acl.js +10 -0
- package/dist/modules/channel_smtp/acl.js.map +7 -0
- package/dist/modules/channel_smtp/capabilities.js +10 -0
- package/dist/modules/channel_smtp/capabilities.js.map +7 -0
- package/dist/modules/channel_smtp/di.js +22 -0
- package/dist/modules/channel_smtp/di.js.map +7 -0
- package/dist/modules/channel_smtp/index.js +13 -0
- package/dist/modules/channel_smtp/index.js.map +7 -0
- package/dist/modules/channel_smtp/integration.js +96 -0
- package/dist/modules/channel_smtp/integration.js.map +7 -0
- package/dist/modules/channel_smtp/lib/adapter.js +127 -0
- package/dist/modules/channel_smtp/lib/adapter.js.map +7 -0
- package/dist/modules/channel_smtp/lib/credentials.js +44 -0
- package/dist/modules/channel_smtp/lib/credentials.js.map +7 -0
- package/dist/modules/channel_smtp/lib/health.js +35 -0
- package/dist/modules/channel_smtp/lib/health.js.map +7 -0
- package/dist/modules/channel_smtp/lib/preset.js +91 -0
- package/dist/modules/channel_smtp/lib/preset.js.map +7 -0
- package/dist/modules/channel_smtp/lib/system-email-config.js +13 -0
- package/dist/modules/channel_smtp/lib/system-email-config.js.map +7 -0
- package/dist/modules/channel_smtp/lib/transport.js +100 -0
- package/dist/modules/channel_smtp/lib/transport.js.map +7 -0
- package/dist/modules/channel_smtp/lib/validate-credentials.js +39 -0
- package/dist/modules/channel_smtp/lib/validate-credentials.js.map +7 -0
- package/dist/modules/channel_smtp/setup.js +30 -0
- package/dist/modules/channel_smtp/setup.js.map +7 -0
- package/jest.config.cjs +33 -0
- package/package.json +86 -3
- package/src/index.ts +1 -0
- package/src/modules/channel_smtp/__integration__/TC-CHANNEL-SMTP-001.spec.ts +133 -0
- package/src/modules/channel_smtp/__integration__/meta.ts +6 -0
- package/src/modules/channel_smtp/__tests__/contracts.test.ts +39 -0
- package/src/modules/channel_smtp/acl.ts +6 -0
- package/src/modules/channel_smtp/capabilities.ts +8 -0
- package/src/modules/channel_smtp/di.ts +20 -0
- package/src/modules/channel_smtp/index.ts +9 -0
- package/src/modules/channel_smtp/integration.ts +91 -0
- package/src/modules/channel_smtp/lib/__tests__/adapter.test.ts +250 -0
- package/src/modules/channel_smtp/lib/__tests__/credentials.test.ts +69 -0
- package/src/modules/channel_smtp/lib/__tests__/health.test.ts +67 -0
- package/src/modules/channel_smtp/lib/__tests__/preset.test.ts +156 -0
- package/src/modules/channel_smtp/lib/__tests__/transport.test.ts +20 -0
- package/src/modules/channel_smtp/lib/__tests__/validate-credentials.test.ts +100 -0
- package/src/modules/channel_smtp/lib/adapter.ts +155 -0
- package/src/modules/channel_smtp/lib/credentials.ts +77 -0
- package/src/modules/channel_smtp/lib/health.ts +47 -0
- package/src/modules/channel_smtp/lib/preset.ts +138 -0
- package/src/modules/channel_smtp/lib/system-email-config.ts +10 -0
- package/src/modules/channel_smtp/lib/transport.ts +166 -0
- package/src/modules/channel_smtp/lib/validate-credentials.ts +60 -0
- package/src/modules/channel_smtp/setup.ts +30 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
- package/README.md +0 -3
package/build.mjs
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
|
|
3
|
+
import { apiRequest } from "@open-mercato/core/helpers/integration/api";
|
|
4
|
+
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
5
|
+
const TENANT_CONNECT = "/api/communication_channels/channels/connect/tenant-credentials";
|
|
6
|
+
const PER_USER_CONNECT = "/api/communication_channels/channels/connect/credentials";
|
|
7
|
+
const validRelay = {
|
|
8
|
+
host: "smtp.example.com",
|
|
9
|
+
port: 587,
|
|
10
|
+
tls: "starttls",
|
|
11
|
+
user: "mailer",
|
|
12
|
+
password: "not-a-real-password",
|
|
13
|
+
fromAddress: "no-reply@example.com"
|
|
14
|
+
};
|
|
15
|
+
test.describe("TC-CHANNEL-SMTP-001: SMTP provider registration and safety refusals", () => {
|
|
16
|
+
test("per-user connect with providerKey=smtp is refused (403, tenant-scoped)", async ({
|
|
17
|
+
request
|
|
18
|
+
}) => {
|
|
19
|
+
const token = await getAuthToken(request);
|
|
20
|
+
const response = await apiRequest(request, "POST", PER_USER_CONNECT, {
|
|
21
|
+
token,
|
|
22
|
+
data: {
|
|
23
|
+
providerKey: "smtp",
|
|
24
|
+
displayName: "SMTP \u2014 per-user (should be refused)",
|
|
25
|
+
credentials: validRelay
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
expect(response.status(), "route should not 5xx").toBeLessThan(500);
|
|
29
|
+
expect(response.status(), "authenticated request should not 401").not.toBe(401);
|
|
30
|
+
expect(response.status()).toBe(403);
|
|
31
|
+
const body = await readJsonSafe(response);
|
|
32
|
+
expect(body?.code).toBe("provider_is_tenant_scoped");
|
|
33
|
+
});
|
|
34
|
+
test("tenant connect with empty credentials reaches validateCredentials (422)", async ({
|
|
35
|
+
request
|
|
36
|
+
}) => {
|
|
37
|
+
const token = await getAuthToken(request);
|
|
38
|
+
const response = await apiRequest(request, "POST", TENANT_CONNECT, {
|
|
39
|
+
token,
|
|
40
|
+
data: { providerKey: "smtp", displayName: "SMTP \u2014 tenant connect", credentials: {} }
|
|
41
|
+
});
|
|
42
|
+
expect(response.status(), "route should not 5xx").toBeLessThan(500);
|
|
43
|
+
expect(response.status(), "authenticated request should not 401").not.toBe(401);
|
|
44
|
+
expect(response.status(), "registered adapter should reject empty credentials").toBe(422);
|
|
45
|
+
const body = await readJsonSafe(response);
|
|
46
|
+
expect(Object.keys(body?.fieldErrors ?? {})).toEqual(
|
|
47
|
+
expect.arrayContaining(["host", "user", "password", "fromAddress"])
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
test("tenant connect refuses a loopback relay host before persisting anything", async ({
|
|
51
|
+
request
|
|
52
|
+
}) => {
|
|
53
|
+
const token = await getAuthToken(request);
|
|
54
|
+
const response = await apiRequest(request, "POST", TENANT_CONNECT, {
|
|
55
|
+
token,
|
|
56
|
+
data: {
|
|
57
|
+
providerKey: "smtp",
|
|
58
|
+
displayName: "SMTP \u2014 loopback relay (should be refused)",
|
|
59
|
+
credentials: { ...validRelay, host: "127.0.0.1" }
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
expect(response.status(), "route should not 5xx").toBeLessThan(500);
|
|
63
|
+
expect(response.status(), "a loopback relay host must be refused at save time").toBe(422);
|
|
64
|
+
const body = await readJsonSafe(response);
|
|
65
|
+
expect(body?.fieldErrors?.host, "the refusal must name the host field").toMatch(
|
|
66
|
+
/private or loopback/i
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
test("tenant connect refuses cleartext transport without the operator opt-in", async ({
|
|
70
|
+
request
|
|
71
|
+
}) => {
|
|
72
|
+
const token = await getAuthToken(request);
|
|
73
|
+
const response = await apiRequest(request, "POST", TENANT_CONNECT, {
|
|
74
|
+
token,
|
|
75
|
+
data: {
|
|
76
|
+
providerKey: "smtp",
|
|
77
|
+
displayName: "SMTP \u2014 cleartext (should be refused)",
|
|
78
|
+
credentials: { ...validRelay, tls: "none", port: 25 }
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
expect(response.status(), "route should not 5xx").toBeLessThan(500);
|
|
82
|
+
expect(response.status(), "cleartext must be refused unless opted in").toBe(422);
|
|
83
|
+
const body = await readJsonSafe(response);
|
|
84
|
+
expect(body?.fieldErrors?.tls).toMatch(/OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT/);
|
|
85
|
+
});
|
|
86
|
+
test("both connect routes reject unauthenticated callers", async ({ request }) => {
|
|
87
|
+
for (const url of [PER_USER_CONNECT, TENANT_CONNECT]) {
|
|
88
|
+
const response = await apiRequest(request, "POST", url, {
|
|
89
|
+
// Intentionally empty token — this asserts the 401 unauth path.
|
|
90
|
+
token: "",
|
|
91
|
+
data: { providerKey: "smtp", displayName: "SMTP", credentials: validRelay }
|
|
92
|
+
});
|
|
93
|
+
expect(response.status(), `${url} must require authentication`).toBe(401);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=TC-CHANNEL-SMTP-001.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_smtp/__integration__/TC-CHANNEL-SMTP-001.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\n/**\n * TC-CHANNEL-SMTP-001 \u2014 SMTP provider registration, tenant scope and the\n * save-time safety refusals.\n *\n * `@open-mercato/channel-smtp` registers the `smtp` email `ChannelAdapter` with\n * `channelScope: 'tenant'`. That declaration is load-bearing: `sendSystemEmail`\n * only ever resolves tenant-wide rows, so a per-user SMTP channel would be\n * invisible to system email. The credential-connect routes are where both the\n * registration and the scope become observable:\n *\n * - the **per-user** route short-circuits tenant-scoped providers with 403\n * (`provider_is_tenant_scoped`) before touching credentials \u2014 403 rather than\n * 404 (`no_adapter`) proves the adapter is registered, and it stops a\n * non-admin minting a tenant-wide relay;\n * - the **tenant** route runs the adapter's own `validateCredentials`, so the\n * relay host is an operator-supplied SSRF surface that must be refused at\n * save time, not merely at send time.\n *\n * Every case here fails validation, so no channel is created and there is\n * nothing to clean up. The relay round-trip (rendering, replyTo, attachments,\n * the raw-string recipient the hub test-send route passes, and partial\n * recipient rejection) is covered network-free in\n * `lib/__tests__/adapter.test.ts` and `lib/__tests__/validate-credentials.test.ts`.\n */\nconst TENANT_CONNECT = '/api/communication_channels/channels/connect/tenant-credentials'\nconst PER_USER_CONNECT = '/api/communication_channels/channels/connect/credentials'\n\nconst validRelay = {\n host: 'smtp.example.com',\n port: 587,\n tls: 'starttls',\n user: 'mailer',\n password: 'not-a-real-password',\n fromAddress: 'no-reply@example.com',\n}\n\ntest.describe('TC-CHANNEL-SMTP-001: SMTP provider registration and safety refusals', () => {\n test('per-user connect with providerKey=smtp is refused (403, tenant-scoped)', async ({\n request,\n }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(request, 'POST', PER_USER_CONNECT, {\n token,\n data: {\n providerKey: 'smtp',\n displayName: 'SMTP \u2014 per-user (should be refused)',\n credentials: validRelay,\n },\n })\n\n expect(response.status(), 'route should not 5xx').toBeLessThan(500)\n expect(response.status(), 'authenticated request should not 401').not.toBe(401)\n expect(response.status()).toBe(403)\n const body = await readJsonSafe<{ code?: string }>(response)\n expect(body?.code).toBe('provider_is_tenant_scoped')\n })\n\n test('tenant connect with empty credentials reaches validateCredentials (422)', async ({\n request,\n }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(request, 'POST', TENANT_CONNECT, {\n token,\n data: { providerKey: 'smtp', displayName: 'SMTP \u2014 tenant connect', credentials: {} },\n })\n\n expect(response.status(), 'route should not 5xx').toBeLessThan(500)\n // Asserting 422 (not merely \"not 404\") proves the request authenticated AND\n // reached the adapter \u2014 a 401 auth misconfig would let this pass vacuously.\n expect(response.status(), 'authenticated request should not 401').not.toBe(401)\n expect(response.status(), 'registered adapter should reject empty credentials').toBe(422)\n const body = await readJsonSafe<{ fieldErrors?: Record<string, string> }>(response)\n expect(Object.keys(body?.fieldErrors ?? {})).toEqual(\n expect.arrayContaining(['host', 'user', 'password', 'fromAddress']),\n )\n })\n\n test('tenant connect refuses a loopback relay host before persisting anything', async ({\n request,\n }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(request, 'POST', TENANT_CONNECT, {\n token,\n data: {\n providerKey: 'smtp',\n displayName: 'SMTP \u2014 loopback relay (should be refused)',\n credentials: { ...validRelay, host: '127.0.0.1' },\n },\n })\n\n expect(response.status(), 'route should not 5xx').toBeLessThan(500)\n expect(response.status(), 'a loopback relay host must be refused at save time').toBe(422)\n const body = await readJsonSafe<{ fieldErrors?: Record<string, string> }>(response)\n expect(body?.fieldErrors?.host, 'the refusal must name the host field').toMatch(\n /private or loopback/i,\n )\n })\n\n test('tenant connect refuses cleartext transport without the operator opt-in', async ({\n request,\n }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(request, 'POST', TENANT_CONNECT, {\n token,\n data: {\n providerKey: 'smtp',\n displayName: 'SMTP \u2014 cleartext (should be refused)',\n credentials: { ...validRelay, tls: 'none', port: 25 },\n },\n })\n\n expect(response.status(), 'route should not 5xx').toBeLessThan(500)\n expect(response.status(), 'cleartext must be refused unless opted in').toBe(422)\n const body = await readJsonSafe<{ fieldErrors?: Record<string, string> }>(response)\n expect(body?.fieldErrors?.tls).toMatch(/OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT/)\n })\n\n test('both connect routes reject unauthenticated callers', async ({ request }) => {\n for (const url of [PER_USER_CONNECT, TENANT_CONNECT]) {\n const response = await apiRequest(request, 'POST', url, {\n // Intentionally empty token \u2014 this asserts the 401 unauth path.\n token: '',\n data: { providerKey: 'smtp', displayName: 'SMTP', credentials: validRelay },\n })\n expect(response.status(), `${url} must require authentication`).toBe(401)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const integrationMeta = {
|
|
2
|
+
description: "SMTP outbound email provider integration coverage (TC-CHANNEL-SMTP-001)",
|
|
3
|
+
dependsOnModules: ["communication_channels", "channel_smtp"]
|
|
4
|
+
};
|
|
5
|
+
var meta_default = integrationMeta;
|
|
6
|
+
export {
|
|
7
|
+
meta_default as default,
|
|
8
|
+
integrationMeta
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_smtp/__integration__/meta.ts"],
|
|
4
|
+
"sourcesContent": ["export const integrationMeta = {\n description: 'SMTP outbound email provider integration coverage (TC-CHANNEL-SMTP-001)',\n dependsOnModules: ['communication_channels', 'channel_smtp'],\n}\n\nexport default integrationMeta\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,kBAAkB;AAAA,EAC7B,aAAa;AAAA,EACb,kBAAkB,CAAC,0BAA0B,cAAc;AAC7D;AAEA,IAAO,eAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const features = [
|
|
2
|
+
{ id: "channel_smtp.view", title: "View SMTP channel configuration", module: "channel_smtp" },
|
|
3
|
+
{ id: "channel_smtp.configure", title: "Configure SMTP channel defaults", module: "channel_smtp" }
|
|
4
|
+
];
|
|
5
|
+
var acl_default = features;
|
|
6
|
+
export {
|
|
7
|
+
acl_default as default,
|
|
8
|
+
features
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=acl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/channel_smtp/acl.ts"],
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'channel_smtp.view', title: 'View SMTP channel configuration', module: 'channel_smtp' },\n { id: 'channel_smtp.configure', title: 'Configure SMTP channel defaults', module: 'channel_smtp' },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,qBAAqB,OAAO,mCAAmC,QAAQ,eAAe;AAAA,EAC5F,EAAE,IAAI,0BAA0B,OAAO,mCAAmC,QAAQ,eAAe;AACnG;AAEA,IAAO,cAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { baseEmailCapabilities } from "@open-mercato/core/modules/communication_channels/lib/email-capabilities";
|
|
2
|
+
const smtpCapabilities = {
|
|
3
|
+
...baseEmailCapabilities,
|
|
4
|
+
fileSharing: false,
|
|
5
|
+
conversationHistory: false
|
|
6
|
+
};
|
|
7
|
+
export {
|
|
8
|
+
smtpCapabilities
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/channel_smtp/capabilities.ts"],
|
|
4
|
+
"sourcesContent": ["import { baseEmailCapabilities } from '@open-mercato/core/modules/communication_channels/lib/email-capabilities'\nimport type { ChannelCapabilities } from '@open-mercato/core/modules/communication_channels/lib/adapter'\n\nexport const smtpCapabilities: ChannelCapabilities = {\n ...baseEmailCapabilities,\n fileSharing: false,\n conversationHistory: false,\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,6BAA6B;AAG/B,MAAM,mBAAwC;AAAA,EACnD,GAAG;AAAA,EACH,aAAa;AAAA,EACb,qBAAqB;AACvB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { asValue } from "awilix";
|
|
2
|
+
import {
|
|
3
|
+
hasChannelAdapter,
|
|
4
|
+
registerChannelAdapter
|
|
5
|
+
} from "@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton";
|
|
6
|
+
import { getSmtpChannelAdapter } from "./lib/adapter.js";
|
|
7
|
+
import { registerSmtpSystemEmailConfigResolver } from "./lib/system-email-config.js";
|
|
8
|
+
import { channelSmtpHealthCheck } from "./lib/health.js";
|
|
9
|
+
function register(container) {
|
|
10
|
+
registerSmtpSystemEmailConfigResolver();
|
|
11
|
+
if (!hasChannelAdapter("smtp")) {
|
|
12
|
+
registerChannelAdapter(getSmtpChannelAdapter());
|
|
13
|
+
}
|
|
14
|
+
container.register({
|
|
15
|
+
channelSmtpAdapter: asValue(getSmtpChannelAdapter()),
|
|
16
|
+
channelSmtpHealthCheck: asValue(channelSmtpHealthCheck)
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
register
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=di.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/channel_smtp/di.ts"],
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n hasChannelAdapter,\n registerChannelAdapter,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton'\nimport { getSmtpChannelAdapter } from './lib/adapter'\nimport { registerSmtpSystemEmailConfigResolver } from './lib/system-email-config'\nimport { channelSmtpHealthCheck } from './lib/health'\n\nexport function register(container: AppContainer): void {\n registerSmtpSystemEmailConfigResolver()\n if (!hasChannelAdapter('smtp')) {\n registerChannelAdapter(getSmtpChannelAdapter())\n }\n container.register({\n channelSmtpAdapter: asValue(getSmtpChannelAdapter()),\n channelSmtpHealthCheck: asValue(channelSmtpHealthCheck),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AACtC,SAAS,6CAA6C;AACtD,SAAS,8BAA8B;AAEhC,SAAS,SAAS,WAA+B;AACtD,wCAAsC;AACtC,MAAI,CAAC,kBAAkB,MAAM,GAAG;AAC9B,2BAAuB,sBAAsB,CAAC;AAAA,EAChD;AACA,YAAU,SAAS;AAAA,IACjB,oBAAoB,QAAQ,sBAAsB,CAAC;AAAA,IACnD,wBAAwB,QAAQ,sBAAsB;AAAA,EACxD,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const metadata = {
|
|
2
|
+
id: "channel_smtp",
|
|
3
|
+
name: "SMTP Email Channel",
|
|
4
|
+
description: "Outbound SMTP relay email channel provider for the Communications Hub.",
|
|
5
|
+
version: "0.1.0",
|
|
6
|
+
requires: ["communication_channels", "integrations"]
|
|
7
|
+
};
|
|
8
|
+
var channel_smtp_default = metadata;
|
|
9
|
+
export {
|
|
10
|
+
channel_smtp_default as default,
|
|
11
|
+
metadata
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/channel_smtp/index.ts"],
|
|
4
|
+
"sourcesContent": ["export const metadata = {\n id: 'channel_smtp',\n name: 'SMTP Email Channel',\n description: 'Outbound SMTP relay email channel provider for the Communications Hub.',\n version: '0.1.0',\n requires: ['communication_channels', 'integrations'],\n}\n\nexport default metadata\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,SAAS;AAAA,EACT,UAAU,CAAC,0BAA0B,cAAc;AACrD;AAEA,IAAO,uBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { buildIntegrationDetailWidgetSpotId } from "@open-mercato/shared/modules/integrations/types";
|
|
2
|
+
const channelSmtpDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId("channel_smtp");
|
|
3
|
+
const integration = {
|
|
4
|
+
id: "channel_smtp",
|
|
5
|
+
title: "SMTP Email",
|
|
6
|
+
description: "Send transactional email through any SMTP relay using the Communications Hub.",
|
|
7
|
+
category: "communication",
|
|
8
|
+
hub: "communication_channels",
|
|
9
|
+
providerKey: "smtp",
|
|
10
|
+
icon: "mail",
|
|
11
|
+
docsUrl: "https://nodemailer.com/smtp/",
|
|
12
|
+
package: "@open-mercato/channel-smtp",
|
|
13
|
+
version: "0.1.0",
|
|
14
|
+
author: "Open Mercato Team",
|
|
15
|
+
company: "Open Mercato",
|
|
16
|
+
license: "MIT",
|
|
17
|
+
tags: ["email", "smtp", "relay", "transactional", "communication"],
|
|
18
|
+
detailPage: {
|
|
19
|
+
widgetSpotId: channelSmtpDetailWidgetSpotId
|
|
20
|
+
},
|
|
21
|
+
apiVersions: [
|
|
22
|
+
{
|
|
23
|
+
id: "smtp",
|
|
24
|
+
label: "SMTP (RFC 5321)",
|
|
25
|
+
status: "stable",
|
|
26
|
+
default: true,
|
|
27
|
+
changelog: "Initial outbound transactional email adapter."
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
healthCheck: { service: "channelSmtpHealthCheck" },
|
|
31
|
+
credentials: {
|
|
32
|
+
fields: [
|
|
33
|
+
{
|
|
34
|
+
key: "host",
|
|
35
|
+
label: "SMTP host",
|
|
36
|
+
type: "text",
|
|
37
|
+
required: true,
|
|
38
|
+
placeholder: "smtp.example.com",
|
|
39
|
+
helpText: "Hostname of the SMTP relay. Private and loopback addresses are rejected."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "port",
|
|
43
|
+
label: "SMTP port",
|
|
44
|
+
type: "text",
|
|
45
|
+
required: true,
|
|
46
|
+
placeholder: "587",
|
|
47
|
+
helpText: "Usually 587 for STARTTLS or 465 for implicit TLS."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "tls",
|
|
51
|
+
label: "Transport security",
|
|
52
|
+
type: "select",
|
|
53
|
+
required: true,
|
|
54
|
+
options: [
|
|
55
|
+
{ value: "starttls", label: "STARTTLS (usually port 587)" },
|
|
56
|
+
{ value: "tls", label: "Implicit TLS (usually port 465)" },
|
|
57
|
+
{ value: "none", label: "Cleartext (requires an operator opt-in)" }
|
|
58
|
+
],
|
|
59
|
+
helpText: "Cleartext is refused unless OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT is set."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "user",
|
|
63
|
+
label: "Username",
|
|
64
|
+
type: "text",
|
|
65
|
+
required: true,
|
|
66
|
+
helpText: "SMTP AUTH username issued by the relay."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "password",
|
|
70
|
+
label: "Password",
|
|
71
|
+
type: "secret",
|
|
72
|
+
required: true,
|
|
73
|
+
helpText: "SMTP AUTH password or app-specific password."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "fromAddress",
|
|
77
|
+
label: "From address",
|
|
78
|
+
type: "text",
|
|
79
|
+
required: true,
|
|
80
|
+
placeholder: "no-reply@example.com",
|
|
81
|
+
helpText: "Sender address the relay is authorized to send for."
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const integrations = [integration];
|
|
87
|
+
const bundles = [];
|
|
88
|
+
const bundle = void 0;
|
|
89
|
+
export {
|
|
90
|
+
bundle,
|
|
91
|
+
bundles,
|
|
92
|
+
channelSmtpDetailWidgetSpotId,
|
|
93
|
+
integration,
|
|
94
|
+
integrations
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=integration.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/channel_smtp/integration.ts"],
|
|
4
|
+
"sourcesContent": ["import { buildIntegrationDetailWidgetSpotId, type IntegrationBundle, type IntegrationDefinition } from '@open-mercato/shared/modules/integrations/types'\n\nexport const channelSmtpDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId('channel_smtp')\n\nexport const integration: IntegrationDefinition = {\n id: 'channel_smtp',\n title: 'SMTP Email',\n description: 'Send transactional email through any SMTP relay using the Communications Hub.',\n category: 'communication',\n hub: 'communication_channels',\n providerKey: 'smtp',\n icon: 'mail',\n docsUrl: 'https://nodemailer.com/smtp/',\n package: '@open-mercato/channel-smtp',\n version: '0.1.0',\n author: 'Open Mercato Team',\n company: 'Open Mercato',\n license: 'MIT',\n tags: ['email', 'smtp', 'relay', 'transactional', 'communication'],\n detailPage: {\n widgetSpotId: channelSmtpDetailWidgetSpotId,\n },\n apiVersions: [\n {\n id: 'smtp',\n label: 'SMTP (RFC 5321)',\n status: 'stable',\n default: true,\n changelog: 'Initial outbound transactional email adapter.',\n },\n ],\n healthCheck: { service: 'channelSmtpHealthCheck' },\n credentials: {\n fields: [\n {\n key: 'host',\n label: 'SMTP host',\n type: 'text',\n required: true,\n placeholder: 'smtp.example.com',\n helpText: 'Hostname of the SMTP relay. Private and loopback addresses are rejected.',\n },\n {\n key: 'port',\n label: 'SMTP port',\n type: 'text',\n required: true,\n placeholder: '587',\n helpText: 'Usually 587 for STARTTLS or 465 for implicit TLS.',\n },\n {\n key: 'tls',\n label: 'Transport security',\n type: 'select',\n required: true,\n options: [\n { value: 'starttls', label: 'STARTTLS (usually port 587)' },\n { value: 'tls', label: 'Implicit TLS (usually port 465)' },\n { value: 'none', label: 'Cleartext (requires an operator opt-in)' },\n ],\n helpText: 'Cleartext is refused unless OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT is set.',\n },\n {\n key: 'user',\n label: 'Username',\n type: 'text',\n required: true,\n helpText: 'SMTP AUTH username issued by the relay.',\n },\n {\n key: 'password',\n label: 'Password',\n type: 'secret',\n required: true,\n helpText: 'SMTP AUTH password or app-specific password.',\n },\n {\n key: 'fromAddress',\n label: 'From address',\n type: 'text',\n required: true,\n placeholder: 'no-reply@example.com',\n helpText: 'Sender address the relay is authorized to send for.',\n },\n ],\n },\n}\n\nexport const integrations: IntegrationDefinition[] = [integration]\nexport const bundles: IntegrationBundle[] = []\nexport const bundle: IntegrationBundle | undefined = undefined\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0CAA8F;AAEhG,MAAM,gCAAgC,mCAAmC,cAAc;AAEvF,MAAM,cAAqC;AAAA,EAChD,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,UAAU;AAAA,EACV,KAAK;AAAA,EACL,aAAa;AAAA,EACb,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM,CAAC,SAAS,QAAQ,SAAS,iBAAiB,eAAe;AAAA,EACjE,YAAY;AAAA,IACV,cAAc;AAAA,EAChB;AAAA,EACA,aAAa;AAAA,IACX;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,aAAa,EAAE,SAAS,yBAAyB;AAAA,EACjD,aAAa;AAAA,IACX,QAAQ;AAAA,MACN;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,SAAS;AAAA,UACP,EAAE,OAAO,YAAY,OAAO,8BAA8B;AAAA,UAC1D,EAAE,OAAO,OAAO,OAAO,kCAAkC;AAAA,UACzD,EAAE,OAAO,QAAQ,OAAO,0CAA0C;AAAA,QACpE;AAAA,QACA,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,eAAwC,CAAC,WAAW;AAC1D,MAAM,UAA+B,CAAC;AACtC,MAAM,SAAwC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
htmlToText,
|
|
3
|
+
sanitizeHeaderValue,
|
|
4
|
+
stringOrUndefined,
|
|
5
|
+
toAddressList
|
|
6
|
+
} from "@open-mercato/core/modules/communication_channels/lib/email-mime";
|
|
7
|
+
import { smtpCapabilities } from "../capabilities.js";
|
|
8
|
+
import { smtpCredentialsSchema } from "./credentials.js";
|
|
9
|
+
import { credentialsToConnection, getSmtpTransport } from "./transport.js";
|
|
10
|
+
import { validateSmtpCredentials } from "./validate-credentials.js";
|
|
11
|
+
function attachmentsFromMeta(value) {
|
|
12
|
+
if (!Array.isArray(value)) return void 0;
|
|
13
|
+
const attachments = value.flatMap((item) => {
|
|
14
|
+
if (!item || typeof item !== "object") return [];
|
|
15
|
+
const record = item;
|
|
16
|
+
const filename = stringOrUndefined(record.filename);
|
|
17
|
+
const content = stringOrUndefined(record.content);
|
|
18
|
+
if (!filename || !content) return [];
|
|
19
|
+
const contentType = stringOrUndefined(record.contentType);
|
|
20
|
+
return [{
|
|
21
|
+
filename,
|
|
22
|
+
content,
|
|
23
|
+
encoding: "base64",
|
|
24
|
+
...contentType ? { contentType } : {}
|
|
25
|
+
}];
|
|
26
|
+
});
|
|
27
|
+
return attachments.length ? attachments : void 0;
|
|
28
|
+
}
|
|
29
|
+
class SmtpChannelAdapter {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.providerKey = "smtp";
|
|
32
|
+
this.channelType = "email";
|
|
33
|
+
this.channelScope = "tenant";
|
|
34
|
+
this.capabilities = smtpCapabilities;
|
|
35
|
+
}
|
|
36
|
+
async sendMessage(input) {
|
|
37
|
+
const credentials = smtpCredentialsSchema.parse(input.credentials);
|
|
38
|
+
const meta = input.metadata ?? {};
|
|
39
|
+
const to = toAddressList(meta.to).map(sanitizeHeaderValue);
|
|
40
|
+
if (to.length === 0) {
|
|
41
|
+
return { externalMessageId: "", status: "failed", error: "[internal] Email send requires at least one recipient" };
|
|
42
|
+
}
|
|
43
|
+
const subject = stringOrUndefined(meta.subject);
|
|
44
|
+
if (!subject) {
|
|
45
|
+
return { externalMessageId: "", status: "failed", error: "[internal] Email send requires a subject" };
|
|
46
|
+
}
|
|
47
|
+
const attachments = attachmentsFromMeta(meta.attachments);
|
|
48
|
+
try {
|
|
49
|
+
const info = await getSmtpTransport().send(credentialsToConnection(credentials), {
|
|
50
|
+
from: stringOrUndefined(meta.from) ?? credentials.fromAddress,
|
|
51
|
+
to,
|
|
52
|
+
subject,
|
|
53
|
+
...input.content.text ? { text: input.content.text } : {},
|
|
54
|
+
...input.content.html ? { html: input.content.html } : {},
|
|
55
|
+
...stringOrUndefined(meta.replyTo) ? { replyTo: stringOrUndefined(meta.replyTo) } : {},
|
|
56
|
+
...attachments?.length ? { attachments } : {}
|
|
57
|
+
});
|
|
58
|
+
const externalMessageId = info.messageId || `smtp:${Date.now()}`;
|
|
59
|
+
const accepted = info.accepted ?? [];
|
|
60
|
+
const rejected = info.rejected ?? [];
|
|
61
|
+
const metadata = {
|
|
62
|
+
...info.response ? { response: info.response } : {},
|
|
63
|
+
...info.accepted ? { accepted } : {},
|
|
64
|
+
...rejected.length ? { rejected } : {}
|
|
65
|
+
};
|
|
66
|
+
if (rejected.length > 0) {
|
|
67
|
+
return {
|
|
68
|
+
externalMessageId,
|
|
69
|
+
conversationId: input.conversationId,
|
|
70
|
+
status: "failed",
|
|
71
|
+
error: `SMTP_SEND_PARTIALLY_REJECTED: relay rejected ${rejected.join(", ")}`,
|
|
72
|
+
metadata
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
externalMessageId,
|
|
77
|
+
conversationId: input.conversationId,
|
|
78
|
+
status: "sent",
|
|
79
|
+
metadata: Object.keys(metadata).length ? metadata : void 0
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
83
|
+
return { externalMessageId: "", status: "failed", error: `SMTP_SEND_FAILED: ${message}` };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async validateCredentials(input) {
|
|
87
|
+
return validateSmtpCredentials(input.credentials);
|
|
88
|
+
}
|
|
89
|
+
async verifyWebhook(_input) {
|
|
90
|
+
return { raw: {}, eventType: "other", metadata: { reason: "smtp-system-email-outbound-only" } };
|
|
91
|
+
}
|
|
92
|
+
async getStatus(_input) {
|
|
93
|
+
return { status: "sent" };
|
|
94
|
+
}
|
|
95
|
+
async normalizeInbound(_raw) {
|
|
96
|
+
throw new Error("[internal] SMTP system email adapter is outbound-only");
|
|
97
|
+
}
|
|
98
|
+
async convertOutbound(input) {
|
|
99
|
+
const meta = input.channelMetadata ?? {};
|
|
100
|
+
const to = toAddressList(meta.to).map(sanitizeHeaderValue);
|
|
101
|
+
const subject = stringOrUndefined(meta.subject);
|
|
102
|
+
const html = input.bodyFormat === "html" ? input.body : void 0;
|
|
103
|
+
const text = input.bodyFormat === "html" ? htmlToText(input.body) : input.body;
|
|
104
|
+
return {
|
|
105
|
+
content: {
|
|
106
|
+
text,
|
|
107
|
+
html,
|
|
108
|
+
bodyFormat: input.bodyFormat
|
|
109
|
+
},
|
|
110
|
+
metadata: {
|
|
111
|
+
to,
|
|
112
|
+
subject,
|
|
113
|
+
from: stringOrUndefined(meta.from),
|
|
114
|
+
replyTo: stringOrUndefined(meta.replyTo),
|
|
115
|
+
attachments: attachmentsFromMeta(meta.attachments)
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const adapter = new SmtpChannelAdapter();
|
|
121
|
+
function getSmtpChannelAdapter() {
|
|
122
|
+
return adapter;
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
getSmtpChannelAdapter
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_smtp/lib/adapter.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n ChannelAdapter,\n ChannelNativeContent,\n ConvertOutboundInput,\n GetMessageStatusInput,\n InboundMessage,\n MessageStatus,\n NormalizedInboundMessage,\n SendMessageInput,\n SendMessageResult,\n ValidateCredentialsInput,\n ValidateCredentialsResult,\n VerifyWebhookInput,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter'\nimport {\n htmlToText,\n sanitizeHeaderValue,\n stringOrUndefined,\n toAddressList,\n} from '@open-mercato/core/modules/communication_channels/lib/email-mime'\nimport { smtpCapabilities } from '../capabilities'\nimport { smtpCredentialsSchema } from './credentials'\nimport { credentialsToConnection, getSmtpTransport, type SmtpAttachment } from './transport'\nimport { validateSmtpCredentials } from './validate-credentials'\n\nfunction attachmentsFromMeta(value: unknown): SmtpAttachment[] | undefined {\n if (!Array.isArray(value)) return undefined\n const attachments = value.flatMap((item): SmtpAttachment[] => {\n if (!item || typeof item !== 'object') return []\n const record = item as Record<string, unknown>\n const filename = stringOrUndefined(record.filename)\n const content = stringOrUndefined(record.content)\n if (!filename || !content) return []\n const contentType = stringOrUndefined(record.contentType)\n return [{\n filename,\n content,\n encoding: 'base64',\n ...(contentType ? { contentType } : {}),\n }]\n })\n return attachments.length ? attachments : undefined\n}\n\nclass SmtpChannelAdapter implements ChannelAdapter {\n readonly providerKey = 'smtp'\n readonly channelType = 'email'\n readonly channelScope = 'tenant' as const\n readonly capabilities = smtpCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n const credentials = smtpCredentialsSchema.parse(input.credentials)\n const meta = (input.metadata ?? {}) as Record<string, unknown>\n // `convertOutbound` hands us an array, but the hub's test-send route calls\n // `sendMessage` directly with the operator's raw `to` string. Normalizing\n // here keeps both callers working; `toAddressList` also splits the\n // comma/semicolon-separated form system email supports.\n const to = toAddressList(meta.to).map(sanitizeHeaderValue)\n if (to.length === 0) {\n return { externalMessageId: '', status: 'failed', error: '[internal] Email send requires at least one recipient' }\n }\n const subject = stringOrUndefined(meta.subject)\n if (!subject) {\n return { externalMessageId: '', status: 'failed', error: '[internal] Email send requires a subject' }\n }\n\n const attachments = attachmentsFromMeta(meta.attachments)\n try {\n const info = await getSmtpTransport().send(credentialsToConnection(credentials), {\n from: stringOrUndefined(meta.from) ?? credentials.fromAddress,\n to,\n subject,\n ...(input.content.text ? { text: input.content.text } : {}),\n ...(input.content.html ? { html: input.content.html } : {}),\n ...(stringOrUndefined(meta.replyTo) ? { replyTo: stringOrUndefined(meta.replyTo) } : {}),\n ...(attachments?.length ? { attachments } : {}),\n })\n const externalMessageId = info.messageId || `smtp:${Date.now()}`\n const accepted = info.accepted ?? []\n const rejected = info.rejected ?? []\n const metadata = {\n ...(info.response ? { response: info.response } : {}),\n ...(info.accepted ? { accepted } : {}),\n ...(rejected.length ? { rejected } : {}),\n }\n // The relay resolves the send once it accepts *any* recipient, so a\n // partial rejection would otherwise be reported as an unqualified\n // success and silently drop mail. Report it as a failure carrying the\n // message identity and the accepted/rejected split, so a retry can skip\n // the recipients that already landed instead of resending to everyone.\n if (rejected.length > 0) {\n return {\n externalMessageId,\n conversationId: input.conversationId,\n status: 'failed',\n error: `SMTP_SEND_PARTIALLY_REJECTED: relay rejected ${rejected.join(', ')}`,\n metadata,\n }\n }\n return {\n externalMessageId,\n conversationId: input.conversationId,\n status: 'sent',\n metadata: Object.keys(metadata).length ? metadata : undefined,\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n return { externalMessageId: '', status: 'failed', error: `SMTP_SEND_FAILED: ${message}` }\n }\n }\n\n async validateCredentials(input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {\n return validateSmtpCredentials(input.credentials)\n }\n\n async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {\n return { raw: {}, eventType: 'other', metadata: { reason: 'smtp-system-email-outbound-only' } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n return { status: 'sent' }\n }\n\n async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {\n throw new Error('[internal] SMTP system email adapter is outbound-only')\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n const meta = (input.channelMetadata ?? {}) as Record<string, unknown>\n const to = toAddressList(meta.to).map(sanitizeHeaderValue)\n const subject = stringOrUndefined(meta.subject)\n const html = input.bodyFormat === 'html' ? input.body : undefined\n const text = input.bodyFormat === 'html' ? htmlToText(input.body) : input.body\n return {\n content: {\n text,\n html,\n bodyFormat: input.bodyFormat,\n },\n metadata: {\n to,\n subject,\n from: stringOrUndefined(meta.from),\n replyTo: stringOrUndefined(meta.replyTo),\n attachments: attachmentsFromMeta(meta.attachments),\n },\n }\n }\n}\n\nconst adapter = new SmtpChannelAdapter()\n\nexport function getSmtpChannelAdapter(): ChannelAdapter {\n return adapter\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
3
|
+
import { isInternalHost } from "@open-mercato/shared/lib/host-pinning";
|
|
4
|
+
const SMTP_TLS_MODES = ["tls", "starttls", "none"];
|
|
5
|
+
function allowsInternalHosts() {
|
|
6
|
+
return parseBooleanWithDefault(process.env.OM_CHANNEL_SMTP_ALLOW_INTERNAL_HOSTS, false);
|
|
7
|
+
}
|
|
8
|
+
function allowsInsecureTransport() {
|
|
9
|
+
return parseBooleanWithDefault(process.env.OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT, false);
|
|
10
|
+
}
|
|
11
|
+
const INTERNAL_HOST_MESSAGE = "Host resolves to a private or loopback address. If this is intentional, an operator must set OM_CHANNEL_SMTP_ALLOW_INTERNAL_HOSTS=true.";
|
|
12
|
+
const UNRESOLVABLE_HOST_MESSAGE = "SMTP host did not resolve to any address.";
|
|
13
|
+
const INSECURE_TRANSPORT_MESSAGE = "Cleartext SMTP sends the password and the message over the wire unencrypted. Use starttls or tls, or set OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT=true.";
|
|
14
|
+
const hostSchema = z.string().trim().min(1, "SMTP host required").max(253, "SMTP host too long").superRefine((value, ctx) => {
|
|
15
|
+
if (allowsInternalHosts()) return;
|
|
16
|
+
if (isInternalHost(value)) {
|
|
17
|
+
ctx.addIssue({
|
|
18
|
+
code: "custom",
|
|
19
|
+
message: "SMTP host appears to point at a private or loopback address. If this is intentional, an operator must set OM_CHANNEL_SMTP_ALLOW_INTERNAL_HOSTS=true."
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const smtpCredentialsSchema = z.object({
|
|
24
|
+
host: hostSchema,
|
|
25
|
+
port: z.coerce.number().int().min(1, "SMTP port must be a positive integer").max(65535, "SMTP port must be <= 65535"),
|
|
26
|
+
tls: z.enum(SMTP_TLS_MODES).superRefine((value, ctx) => {
|
|
27
|
+
if (value !== "none") return;
|
|
28
|
+
if (allowsInsecureTransport()) return;
|
|
29
|
+
ctx.addIssue({ code: "custom", message: INSECURE_TRANSPORT_MESSAGE });
|
|
30
|
+
}),
|
|
31
|
+
user: z.string().min(1, "SMTP username required"),
|
|
32
|
+
password: z.string().min(1, "SMTP password required"),
|
|
33
|
+
fromAddress: z.string().email("From address must be a valid email")
|
|
34
|
+
}).passthrough();
|
|
35
|
+
export {
|
|
36
|
+
INSECURE_TRANSPORT_MESSAGE,
|
|
37
|
+
INTERNAL_HOST_MESSAGE,
|
|
38
|
+
SMTP_TLS_MODES,
|
|
39
|
+
UNRESOLVABLE_HOST_MESSAGE,
|
|
40
|
+
allowsInsecureTransport,
|
|
41
|
+
allowsInternalHosts,
|
|
42
|
+
smtpCredentialsSchema
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_smtp/lib/credentials.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { isInternalHost } from '@open-mercato/shared/lib/host-pinning'\n\nexport const SMTP_TLS_MODES = ['tls', 'starttls', 'none'] as const\n\nexport type SmtpTlsMode = (typeof SMTP_TLS_MODES)[number]\n\nexport function allowsInternalHosts(): boolean {\n return parseBooleanWithDefault(process.env.OM_CHANNEL_SMTP_ALLOW_INTERNAL_HOSTS, false)\n}\n\nexport function allowsInsecureTransport(): boolean {\n return parseBooleanWithDefault(process.env.OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT, false)\n}\n\nexport const INTERNAL_HOST_MESSAGE =\n 'Host resolves to a private or loopback address. If this is intentional, an operator must set OM_CHANNEL_SMTP_ALLOW_INTERNAL_HOSTS=true.'\n\nexport const UNRESOLVABLE_HOST_MESSAGE = 'SMTP host did not resolve to any address.'\n\nexport const INSECURE_TRANSPORT_MESSAGE =\n 'Cleartext SMTP sends the password and the message over the wire unencrypted. Use starttls or tls, or set OM_CHANNEL_SMTP_ALLOW_INSECURE_TRANSPORT=true.'\n\n/**\n * The relay host is operator-supplied and dialled as a raw socket, so it gets the\n * same two-layer SSRF treatment as IMAP: this cheap string check at save time, and\n * DNS-rebinding-safe pinning at connect time in `transport.ts`. Both layers live in\n * `@open-mercato/shared/lib/host-pinning`.\n */\nconst hostSchema = z\n .string()\n .trim()\n .min(1, 'SMTP host required')\n .max(253, 'SMTP host too long')\n .superRefine((value, ctx) => {\n if (allowsInternalHosts()) return\n if (isInternalHost(value)) {\n ctx.addIssue({\n code: 'custom',\n message:\n 'SMTP host appears to point at a private or loopback address. If this is intentional, an operator must set OM_CHANNEL_SMTP_ALLOW_INTERNAL_HOSTS=true.',\n })\n }\n })\n\n/**\n * Tenant-wide SMTP relay credentials. Persisted inside\n * `IntegrationCredentials.credentials` (encrypted at rest) and re-parsed before\n * every send, so a relay reconfigured to cleartext behind our back is rejected on\n * the next send rather than only at setup time.\n *\n * Never log credential values.\n */\nexport const smtpCredentialsSchema = z\n .object({\n host: hostSchema,\n port: z.coerce\n .number()\n .int()\n .min(1, 'SMTP port must be a positive integer')\n .max(65535, 'SMTP port must be <= 65535'),\n tls: z.enum(SMTP_TLS_MODES).superRefine((value, ctx) => {\n if (value !== 'none') return\n if (allowsInsecureTransport()) return\n ctx.addIssue({ code: 'custom', message: INSECURE_TRANSPORT_MESSAGE })\n }),\n user: z.string().min(1, 'SMTP username required'),\n password: z.string().min(1, 'SMTP password required'),\n fromAddress: z.string().email('From address must be a valid email'),\n })\n // `.passthrough()` rather than `.strict()`: the integrations credential store\n // stashes bookkeeping keys alongside the operator-entered fields, and a strict\n // schema would reject the blob it just persisted.\n .passthrough()\n\nexport type SmtpCredentials = z.infer<typeof smtpCredentialsSchema>\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,+BAA+B;AACxC,SAAS,sBAAsB;AAExB,MAAM,iBAAiB,CAAC,OAAO,YAAY,MAAM;AAIjD,SAAS,sBAA+B;AAC7C,SAAO,wBAAwB,QAAQ,IAAI,sCAAsC,KAAK;AACxF;AAEO,SAAS,0BAAmC;AACjD,SAAO,wBAAwB,QAAQ,IAAI,0CAA0C,KAAK;AAC5F;AAEO,MAAM,wBACX;AAEK,MAAM,4BAA4B;AAElC,MAAM,6BACX;AAQF,MAAM,aAAa,EAChB,OAAO,EACP,KAAK,EACL,IAAI,GAAG,oBAAoB,EAC3B,IAAI,KAAK,oBAAoB,EAC7B,YAAY,CAAC,OAAO,QAAQ;AAC3B,MAAI,oBAAoB,EAAG;AAC3B,MAAI,eAAe,KAAK,GAAG;AACzB,QAAI,SAAS;AAAA,MACX,MAAM;AAAA,MACN,SACE;AAAA,IACJ,CAAC;AAAA,EACH;AACF,CAAC;AAUI,MAAM,wBAAwB,EAClC,OAAO;AAAA,EACN,MAAM;AAAA,EACN,MAAM,EAAE,OACL,OAAO,EACP,IAAI,EACJ,IAAI,GAAG,sCAAsC,EAC7C,IAAI,OAAO,4BAA4B;AAAA,EAC1C,KAAK,EAAE,KAAK,cAAc,EAAE,YAAY,CAAC,OAAO,QAAQ;AACtD,QAAI,UAAU,OAAQ;AACtB,QAAI,wBAAwB,EAAG;AAC/B,QAAI,SAAS,EAAE,MAAM,UAAU,SAAS,2BAA2B,CAAC;AAAA,EACtE,CAAC;AAAA,EACD,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,wBAAwB;AAAA,EAChD,UAAU,EAAE,OAAO,EAAE,IAAI,GAAG,wBAAwB;AAAA,EACpD,aAAa,EAAE,OAAO,EAAE,MAAM,oCAAoC;AACpE,CAAC,EAIA,YAAY;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { smtpCredentialsSchema } from "./credentials.js";
|
|
2
|
+
import { credentialsToConnection, getSmtpTransport } from "./transport.js";
|
|
3
|
+
const PROBE_TIMEOUT_MS = 8e3;
|
|
4
|
+
const channelSmtpHealthCheck = {
|
|
5
|
+
async check(credentials, _scope) {
|
|
6
|
+
const parsed = smtpCredentialsSchema.safeParse(credentials ?? {});
|
|
7
|
+
if (!parsed.success) {
|
|
8
|
+
return {
|
|
9
|
+
status: "unhealthy",
|
|
10
|
+
message: `SMTP credentials invalid: ${parsed.error.issues[0]?.message ?? "unknown validation error"}`,
|
|
11
|
+
details: { reason: "invalid_credentials" }
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const connection = { ...credentialsToConnection(parsed.data), timeoutMs: PROBE_TIMEOUT_MS };
|
|
15
|
+
try {
|
|
16
|
+
await getSmtpTransport().verify(connection);
|
|
17
|
+
return {
|
|
18
|
+
status: "healthy",
|
|
19
|
+
message: "SMTP relay accepted the credentials",
|
|
20
|
+
details: { host: connection.host, port: connection.port, tls: connection.tls }
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : "SMTP connection failed";
|
|
24
|
+
return {
|
|
25
|
+
status: "unhealthy",
|
|
26
|
+
message: `SMTP health check failed: ${message}`,
|
|
27
|
+
details: { reason: "connection_failed", host: connection.host, port: connection.port }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export {
|
|
33
|
+
channelSmtpHealthCheck
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=health.js.map
|