feat(iios): register EmailAdapter for EMAIL + email-shaped egress provider
AdapterRegistry now backs the EMAIL channel with the email-native EmailAdapter
(RFC threading) instead of the generic WebhookAdapter. Adds EmailProvider — an
email-envelope egress provider ({to,subject,text,html,inReplyTo}) registered for
EMAIL when IIOS_PROVIDER_URL_EMAIL is set (sandbox stays the default). Spec:
signed email → normalized interaction on the email thread; a reply joins the
same thread (one conversation); bad signature → rejected (fail-closed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { CapabilityProvider } from '@insignia/iios-contracts';
|
||||
import { SandboxProvider } from './sandbox.provider';
|
||||
import { HttpProvider } from './http.provider';
|
||||
import { EmailProvider } from './email.provider';
|
||||
|
||||
const DEFAULT_CHANNELS = ['WEBHOOK', 'EMAIL', 'WHATSAPP', 'PORTAL'];
|
||||
|
||||
@@ -19,7 +20,9 @@ export class CapabilityProviderRegistry {
|
||||
for (const ch of DEFAULT_CHANNELS) this.register(new SandboxProvider([ch]));
|
||||
for (const ch of DEFAULT_CHANNELS) {
|
||||
const url = process.env[`IIOS_PROVIDER_URL_${ch}`];
|
||||
if (url) this.register(new HttpProvider(ch, url));
|
||||
if (!url) continue;
|
||||
// EMAIL gets an email-shaped envelope provider; other channels use the generic HTTP one.
|
||||
this.register(ch === 'EMAIL' ? new EmailProvider(url) : new HttpProvider(ch, url));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { CapabilityProvider, CapabilityRequest, ProviderResult } from '@insignia/iios-contracts';
|
||||
|
||||
interface EmailPayload {
|
||||
subject?: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
inReplyTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email egress provider: shapes the outbound command into an email envelope
|
||||
* ({to, subject, text, html, inReplyTo}) and POSTs it to a configured send endpoint
|
||||
* (a vendor mail API / SMTP relay behind a URL). Activated only when
|
||||
* IIOS_PROVIDER_URL_EMAIL is set; a transport failure is surfaced as FAILED, never thrown.
|
||||
*/
|
||||
export class EmailProvider implements CapabilityProvider {
|
||||
readonly name = 'email-http';
|
||||
readonly channelTypes = ['EMAIL'];
|
||||
readonly capabilities = { canSend: true };
|
||||
|
||||
constructor(private readonly url: string) {}
|
||||
|
||||
async send(req: CapabilityRequest): Promise<ProviderResult> {
|
||||
const started = Date.now();
|
||||
const p = (req.payload ?? {}) as EmailPayload;
|
||||
const email = { to: req.target, subject: p.subject ?? '(no subject)', text: p.text, html: p.html, inReplyTo: p.inReplyTo };
|
||||
try {
|
||||
const res = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'idempotency-key': req.idempotencyKey },
|
||||
body: JSON.stringify(email),
|
||||
});
|
||||
const latencyMs = Date.now() - started;
|
||||
if (!res.ok) return { providerRef: `email-${res.status}`, outcome: 'FAILED', errorCode: `HTTP_${res.status}`, latencyMs };
|
||||
return { providerRef: `email-${req.idempotencyKey}`, outcome: 'SENT', latencyMs };
|
||||
} catch (err) {
|
||||
return { providerRef: 'email-error', outcome: 'FAILED', errorCode: (err as Error).message.slice(0, 60), latencyMs: Date.now() - started };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user