@fabrika/email 0.9.0-dev.1 → 0.9.0-dev.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/email",
3
- "version": "0.9.0-dev.1",
3
+ "version": "0.9.0-dev.3",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,47 +12,48 @@
12
12
  },
13
13
  "files": [
14
14
  "src",
15
- "!src/**/__tests__/**"
15
+ "!src/**/__tests__/**",
16
+ "types"
16
17
  ],
17
18
  "exports": {
18
19
  ".": {
19
20
  "bun": "./src/index.ts",
20
- "types": "./src/index.ts",
21
+ "types": "./types/src/index.d.ts",
21
22
  "default": "./src/index.ts"
22
23
  },
23
24
  "./resend": {
24
25
  "bun": "./src/resend.ts",
25
- "types": "./src/resend.ts",
26
+ "types": "./types/src/resend.d.ts",
26
27
  "default": "./src/resend.ts"
27
28
  },
28
29
  "./directory": {
29
30
  "bun": "./src/directory.ts",
30
- "types": "./src/directory.ts",
31
+ "types": "./types/src/directory.d.ts",
31
32
  "default": "./src/directory.ts"
32
33
  },
33
34
  "./memory": {
34
35
  "bun": "./src/memory.ts",
35
- "types": "./src/memory.ts",
36
+ "types": "./types/src/memory.d.ts",
36
37
  "default": "./src/memory.ts"
37
38
  },
38
39
  "./smartemailing": {
39
40
  "bun": "./src/smartemailing.ts",
40
- "types": "./src/smartemailing.ts",
41
+ "types": "./types/src/smartemailing.d.ts",
41
42
  "default": "./src/smartemailing.ts"
42
43
  },
43
44
  "./providers": {
44
45
  "bun": "./src/providers.ts",
45
- "types": "./src/providers.ts",
46
+ "types": "./types/src/providers.d.ts",
46
47
  "default": "./src/providers.ts"
47
48
  },
48
49
  "./platform": {
49
50
  "bun": "./src/platform.ts",
50
- "types": "./src/platform.ts",
51
+ "types": "./types/src/platform.d.ts",
51
52
  "default": "./src/platform.ts"
52
53
  },
53
54
  "./conformance": {
54
55
  "bun": "./src/conformance.ts",
55
- "types": "./src/conformance.ts",
56
+ "types": "./types/src/conformance.d.ts",
56
57
  "default": "./src/conformance.ts"
57
58
  }
58
59
  },
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import { type EmailFetch, type EmailSender } from './email.js';
3
+ export type EmailEnvironment = Readonly<Record<string, string | undefined>>;
4
+ export interface EmailSenderIdentity {
5
+ readonly address: string;
6
+ readonly name?: string;
7
+ }
8
+ export interface EmailProviderVariable {
9
+ readonly name: string;
10
+ readonly secret: boolean;
11
+ readonly required: boolean;
12
+ }
13
+ export interface EmailProvider {
14
+ /** Provider-specific variables, beside FABRIKA_EMAIL_PROVIDER/FROM/FROM_NAME. */
15
+ readonly variables: readonly EmailProviderVariable[];
16
+ /** Parses its own variables; throws `invalid_configuration` naming the variable, never its value. */
17
+ create(input: {
18
+ readonly from: EmailSenderIdentity;
19
+ readonly env: EmailEnvironment;
20
+ readonly fetch?: EmailFetch;
21
+ }): EmailSender;
22
+ }
23
+ export type EmailProviderRegistry = Readonly<Record<string, EmailProvider>>;
24
+ export type EmailTransportConfig = {
25
+ readonly kind: 'none';
26
+ } | {
27
+ readonly kind: 'provider';
28
+ readonly provider: string;
29
+ readonly from: EmailSenderIdentity;
30
+ readonly variables: readonly EmailProviderVariable[];
31
+ };
32
+ /** The variable that selects the provider. */
33
+ export declare const EMAIL_PROVIDER_VARIABLE = "FABRIKA_EMAIL_PROVIDER";
34
+ /** `FABRIKA_EMAIL_PROVIDER=none`: the installation states it has no transport, which an absent variable does not. */
35
+ export declare const NO_EMAIL_PROVIDER = "none";
36
+ /** Throws one `invalid_configuration` listing every missing or invalid variable name. */
37
+ export declare function readEmailTransportConfig(env: EmailEnvironment, registry: EmailProviderRegistry): EmailTransportConfig;
38
+ /** null for `none` or an absent provider. */
39
+ export declare function emailSenderFromEnv(env: EmailEnvironment, registry: EmailProviderRegistry, options?: {
40
+ readonly fetch?: EmailFetch;
41
+ }): EmailSender | null;
42
+ /** The non-empty string `FABRIKA_EMAIL_*` entries of any binding object (Worker bindings, process.env). */
43
+ export declare function pickEmailEnvironment(source: object): EmailEnvironment;
44
+ /**
45
+ * A provider's own variables through its schema. A failure is one `invalid_configuration` naming each offending
46
+ * variable and never its value, in the form `readEmailTransportConfig` merges into its own refusal.
47
+ */
48
+ export declare function parseProviderVariables<Schema extends z.ZodTypeAny>(schema: Schema, env: EmailEnvironment): z.output<Schema>;
@@ -0,0 +1,57 @@
1
+ import { type EmailAttachmentContent } from './content.js';
2
+ import { type EmailFetch, type EmailMessage, type EmailSender } from './email.js';
3
+ /** What the provider received, or what a local sender stored, in the message's own terms. */
4
+ export interface WireMessage {
5
+ readonly to: string;
6
+ readonly subject: string;
7
+ readonly text: string;
8
+ readonly html?: string;
9
+ readonly replyTo?: string;
10
+ readonly fromName?: string;
11
+ readonly idempotencyKey?: string;
12
+ /** The URL of the `List-Unsubscribe` header, when `List-Unsubscribe-Post` says one click. */
13
+ readonly unsubscribeUrl?: string;
14
+ /** Each attachment with its bytes as base64. */
15
+ readonly attachments?: readonly EmailAttachmentContent[];
16
+ }
17
+ /** A stored or sent message in wire terms: what a local harness answers for an `EmailMessage` it kept. */
18
+ export declare function wireMessageOf(message: EmailMessage): WireMessage;
19
+ /** How the provider answers. `inProgress` and `idempotencyConflict` are required when the sender is idempotent. */
20
+ export interface ProviderAnswers {
21
+ accepted(request: Request): Response;
22
+ malformedSuccess(): Response;
23
+ unauthorized(): Response;
24
+ rejected(): Response;
25
+ rateLimited(): Response;
26
+ unavailable(): Response;
27
+ quotaExceeded?(): Response;
28
+ inProgress?(): Response;
29
+ idempotencyConflict?(): Response;
30
+ }
31
+ export interface EmailSenderHttpHarness {
32
+ readonly kind: 'http';
33
+ /** Credentials the sender holds; none may appear in an error or a log line. */
34
+ readonly secrets: readonly string[];
35
+ create(fetch: EmailFetch, options: {
36
+ readonly timeoutMs: number;
37
+ }): EmailSender;
38
+ readRequest(request: Request): Promise<WireMessage>;
39
+ readonly answers: ProviderAnswers;
40
+ }
41
+ /** A local sender's I/O cannot be held open from outside, so the abort-during-I/O cases run for HTTP senders only. */
42
+ export interface EmailSenderLocalHarness {
43
+ readonly kind: 'local';
44
+ /** Called before each case. */
45
+ create(): Promise<EmailSender>;
46
+ /** What the sender created by the last `create` stored, oldest first. */
47
+ stored(): Promise<readonly WireMessage[]>;
48
+ /** Called after each case. */
49
+ cleanup?(): Promise<void>;
50
+ }
51
+ export type EmailSenderConformanceHarness = EmailSenderHttpHarness | EmailSenderLocalHarness;
52
+ /**
53
+ * Registers the conformance cases for one sender. HTTP harnesses also get the provider answer table, the thrown and
54
+ * silent fetch, and an abort or a timeout while the request or the answer body is in flight; a local harness gets
55
+ * only the already-aborted case, because nothing can hold its I/O open.
56
+ */
57
+ export declare function describeEmailSenderConformance(name: string, harness: EmailSenderConformanceHarness): void;
@@ -0,0 +1,22 @@
1
+ import type { EmailMessage } from './email.js';
2
+ /** An attachment as a local sender keeps and compares it: the bytes as base64. */
3
+ export interface EmailAttachmentContent {
4
+ readonly filename: string;
5
+ readonly contentType: string;
6
+ readonly content: string;
7
+ }
8
+ /** What a local sender compares when one idempotency key arrives twice: everything the recipient would see. */
9
+ export interface EmailContent {
10
+ readonly to: string;
11
+ readonly subject: string;
12
+ readonly text: string;
13
+ readonly html?: string;
14
+ readonly replyTo?: string;
15
+ readonly fromName?: string;
16
+ readonly unsubscribeUrl?: string;
17
+ readonly attachments?: readonly EmailAttachmentContent[];
18
+ }
19
+ export declare function sameEmailContent(left: EmailContent, right: EmailContent): boolean;
20
+ export declare function emailContentOf(message: EmailMessage): EmailContent;
21
+ /** `bytes` as standard base64, on every runtime: `btoa` over chunks, so a large file never overflows the call stack. */
22
+ export declare function base64Of(bytes: Uint8Array): string;
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+ import { type EmailProvider } from './config.js';
3
+ import { type EmailDeliveryResult, type EmailMessage, type EmailSender, type EmailSenderCapabilities } from './email.js';
4
+ declare const capturedEmailSchema: z.ZodObject<{
5
+ messageId: z.ZodString;
6
+ idempotencyKey: z.ZodString;
7
+ to: z.ZodString;
8
+ subject: z.ZodString;
9
+ text: z.ZodString;
10
+ html: z.ZodOptional<z.ZodString>;
11
+ replyTo: z.ZodOptional<z.ZodString>;
12
+ fromName: z.ZodOptional<z.ZodString>;
13
+ unsubscribeUrl: z.ZodOptional<z.ZodString>;
14
+ attachments: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodObject<{
15
+ filename: z.ZodString;
16
+ contentType: z.ZodString;
17
+ content: z.ZodString;
18
+ }, "strip", z.ZodTypeAny, {
19
+ filename: string;
20
+ contentType: string;
21
+ content: string;
22
+ }, {
23
+ filename: string;
24
+ contentType: string;
25
+ content: string;
26
+ }>, "many">>>;
27
+ capturedAt: z.ZodNumber;
28
+ }, "strip", z.ZodTypeAny, {
29
+ messageId: string;
30
+ idempotencyKey: string;
31
+ to: string;
32
+ subject: string;
33
+ text: string;
34
+ capturedAt: number;
35
+ html?: string | undefined;
36
+ replyTo?: string | undefined;
37
+ fromName?: string | undefined;
38
+ unsubscribeUrl?: string | undefined;
39
+ attachments?: readonly {
40
+ filename: string;
41
+ contentType: string;
42
+ content: string;
43
+ }[] | undefined;
44
+ }, {
45
+ messageId: string;
46
+ idempotencyKey: string;
47
+ to: string;
48
+ subject: string;
49
+ text: string;
50
+ capturedAt: number;
51
+ html?: string | undefined;
52
+ replyTo?: string | undefined;
53
+ fromName?: string | undefined;
54
+ unsubscribeUrl?: string | undefined;
55
+ attachments?: readonly {
56
+ filename: string;
57
+ contentType: string;
58
+ content: string;
59
+ }[] | undefined;
60
+ }>;
61
+ export type CapturedEmail = z.infer<typeof capturedEmailSchema>;
62
+ export interface DirectoryEmailSenderOptions {
63
+ /** An existing directory, as an absolute path. */
64
+ readonly directory: string;
65
+ readonly now?: () => number;
66
+ }
67
+ export declare class DirectoryEmailSender implements EmailSender {
68
+ #private;
69
+ readonly capabilities: EmailSenderCapabilities;
70
+ constructor(options: DirectoryEmailSenderOptions);
71
+ /** One file per idempotency key: a repeated send of the same message answers the first id, as a provider does. */
72
+ send(message: EmailMessage, signal: AbortSignal): Promise<EmailDeliveryResult>;
73
+ }
74
+ /**
75
+ * `FABRIKA_EMAIL_PROVIDER=directory`: a local composition's capture. Never in `builtinEmailProviders`; a composition
76
+ * registers it. The capture records content only, so `FABRIKA_EMAIL_FROM` is not used.
77
+ */
78
+ export declare const directoryEmailProvider: EmailProvider;
79
+ /** Every message a `DirectoryEmailSender` captured in `directory`, oldest first. */
80
+ export declare function readCapturedEmails(directory: string): Promise<CapturedEmail[]>;
81
+ export {};
@@ -0,0 +1,130 @@
1
+ export type EmailFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
2
+ /** One file a message carries: the name the recipient sees, its media type and its bytes. */
3
+ export interface EmailAttachment {
4
+ readonly filename: string;
5
+ readonly contentType: string;
6
+ readonly content: Uint8Array;
7
+ }
8
+ export interface EmailMessage {
9
+ readonly to: string;
10
+ readonly subject: string;
11
+ readonly text: string;
12
+ readonly html?: string;
13
+ /** 1..256 visible ASCII characters (Resend's limit). A prefix the caller adds must fit inside it. */
14
+ readonly idempotencyKey: string;
15
+ /** One mailbox address. */
16
+ readonly replyTo?: string;
17
+ /** Replaces the transport's display name for this message. */
18
+ readonly fromName?: string;
19
+ /**
20
+ * An `https:` URL that unsubscribes the recipient in one click (RFC 8058): the transport writes `List-Unsubscribe:
21
+ * <url>` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. A transport without `listUnsubscribe` refuses it.
22
+ */
23
+ readonly unsubscribeUrl?: string;
24
+ /**
25
+ * Files the message carries, at most `MAX_EMAIL_ATTACHMENTS`, together at most the transport's `maxAttachmentBytes`.
26
+ * A transport without `maxAttachmentBytes` refuses a message that has any; an empty list is none.
27
+ */
28
+ readonly attachments?: readonly EmailAttachment[];
29
+ }
30
+ export interface EmailDeliveryResult {
31
+ readonly status: 'accepted';
32
+ readonly provider: string;
33
+ readonly messageId: string;
34
+ }
35
+ export interface EmailSenderCapabilities {
36
+ /** A repeated send under one key with the same message is not delivered again. */
37
+ readonly idempotent: boolean;
38
+ /** 'sync': `accepted` means the provider took the message. 'queued': it queued the request and may still drop it. */
39
+ readonly confirmation: 'sync' | 'queued';
40
+ /** The most messages one call carries. `send` carries one, so every built-in adapter reports 1. */
41
+ readonly maxBatch: number;
42
+ /** Whether it writes a message's one-click unsubscribe headers (`unsubscribeUrl`); absent is no. */
43
+ readonly listUnsubscribe?: boolean;
44
+ /** The most attachment bytes one message carries, every file together; absent: it sends no attachments. */
45
+ readonly maxAttachmentBytes?: number;
46
+ }
47
+ export interface EmailSender {
48
+ readonly capabilities: EmailSenderCapabilities;
49
+ send(message: EmailMessage, signal: AbortSignal): Promise<EmailDeliveryResult>;
50
+ }
51
+ /** 'not_sent': the provider did not take the message. 'unknown': it may have taken it. */
52
+ export type EmailDeliveryOutcome = 'not_sent' | 'unknown';
53
+ /** The one classification of every failure: an adapter names the code, and the code decides the rest. */
54
+ export declare const EMAIL_DELIVERY_ERRORS: {
55
+ readonly invalid_configuration: {
56
+ readonly retryable: false;
57
+ readonly outcome: "not_sent";
58
+ };
59
+ readonly invalid_message: {
60
+ readonly retryable: false;
61
+ readonly outcome: "not_sent";
62
+ };
63
+ readonly mail_not_configured: {
64
+ readonly retryable: false;
65
+ readonly outcome: "not_sent";
66
+ };
67
+ readonly aborted: {
68
+ readonly retryable: true;
69
+ readonly outcome: "unknown";
70
+ };
71
+ readonly network_error: {
72
+ readonly retryable: true;
73
+ readonly outcome: "unknown";
74
+ };
75
+ readonly request_timeout: {
76
+ readonly retryable: true;
77
+ readonly outcome: "unknown";
78
+ };
79
+ readonly request_in_progress: {
80
+ readonly retryable: true;
81
+ readonly outcome: "unknown";
82
+ };
83
+ readonly idempotency_conflict: {
84
+ readonly retryable: false;
85
+ readonly outcome: "unknown";
86
+ };
87
+ readonly provider_unauthorized: {
88
+ readonly retryable: false;
89
+ readonly outcome: "not_sent";
90
+ };
91
+ readonly rate_limited: {
92
+ readonly retryable: true;
93
+ readonly outcome: "not_sent";
94
+ };
95
+ readonly quota_exceeded: {
96
+ readonly retryable: true;
97
+ readonly outcome: "not_sent";
98
+ };
99
+ readonly provider_rejected: {
100
+ readonly retryable: false;
101
+ readonly outcome: "not_sent";
102
+ };
103
+ readonly provider_unavailable: {
104
+ readonly retryable: true;
105
+ readonly outcome: "unknown";
106
+ };
107
+ readonly malformed_response: {
108
+ readonly retryable: true;
109
+ readonly outcome: "unknown";
110
+ };
111
+ };
112
+ export type EmailDeliveryErrorCode = keyof typeof EMAIL_DELIVERY_ERRORS;
113
+ export interface EmailDeliveryErrorOptions {
114
+ readonly code: EmailDeliveryErrorCode;
115
+ readonly httpStatus?: number;
116
+ }
117
+ /** Its message is a fixed string plus the HTTP status: never a credential, a response body, a subject or a body. */
118
+ export declare class EmailDeliveryError extends Error {
119
+ readonly code: EmailDeliveryErrorCode;
120
+ readonly retryable: boolean;
121
+ readonly outcome: EmailDeliveryOutcome;
122
+ readonly httpStatus: number | undefined;
123
+ constructor(message: string, options: EmailDeliveryErrorOptions);
124
+ }
125
+ /** The two headers of a one-click unsubscribe (RFC 8058), as a transport writes them for `unsubscribeUrl`. */
126
+ export declare function oneClickUnsubscribeHeaders(url: string): Readonly<Record<string, string>>;
127
+ /** The one retry rule: an unknown outcome is retried only over an idempotent transport. */
128
+ export declare function mayRetryDelivery(error: EmailDeliveryError, capabilities: EmailSenderCapabilities): boolean;
129
+ /** The bytes of every attachment of `message` together. */
130
+ export declare function attachmentBytes(message: EmailMessage): number;
@@ -0,0 +1,20 @@
1
+ import { type EmailFetch } from './email.js';
2
+ export interface ProviderRequest {
3
+ /** The provider's name as it appears in error messages. */
4
+ readonly label: string;
5
+ readonly fetch: EmailFetch;
6
+ readonly url: string;
7
+ readonly headers: Readonly<Record<string, string>>;
8
+ readonly body: unknown;
9
+ readonly signal: AbortSignal;
10
+ readonly timeoutMs: number;
11
+ }
12
+ export interface ProviderResponse {
13
+ readonly ok: boolean;
14
+ readonly status: number;
15
+ /** The parsed JSON body, or undefined when it is not JSON. Never put into an error. */
16
+ readonly body: unknown;
17
+ }
18
+ /** The answer with its body already read, so an abort or a timeout while the body arrives is classified as one. */
19
+ export declare function postToProvider(request: ProviderRequest): Promise<ProviderResponse>;
20
+ export declare function parseTimeout(label: string, value: number | undefined): number;
@@ -0,0 +1,5 @@
1
+ export { EMAIL_PROVIDER_VARIABLE, emailSenderFromEnv, NO_EMAIL_PROVIDER, parseProviderVariables, pickEmailEnvironment, readEmailTransportConfig, } from './config.js';
2
+ export type { EmailEnvironment, EmailProvider, EmailProviderRegistry, EmailProviderVariable, EmailSenderIdentity, EmailTransportConfig, } from './config.js';
3
+ export { attachmentBytes, EMAIL_DELIVERY_ERRORS, EmailDeliveryError, mayRetryDelivery, oneClickUnsubscribeHeaders } from './email.js';
4
+ export type { EmailAttachment, EmailDeliveryErrorCode, EmailDeliveryErrorOptions, EmailDeliveryOutcome, EmailDeliveryResult, EmailFetch, EmailMessage, EmailSender, EmailSenderCapabilities, } from './email.js';
5
+ export { isUnsubscribeUrl, isValidAttachmentFilename, isValidEmailAddress, isValidEmailDisplayName, isValidEmailFrom, MAX_EMAIL_ATTACHMENTS, MAX_EMAIL_SUBJECT_LENGTH, MAX_UNSUBSCRIBE_URL_LENGTH, validateEmailAttachments, validateEmailMessage, } from './validation.js';
@@ -0,0 +1,42 @@
1
+ import { type EmailDeliveryErrorCode, type EmailDeliveryResult, type EmailMessage, type EmailSender, type EmailSenderCapabilities } from './email.js';
2
+ export interface MemoryEmail {
3
+ readonly message: EmailMessage;
4
+ readonly messageId: string;
5
+ readonly acceptedAt: number;
6
+ }
7
+ export interface MemoryHandoff {
8
+ readonly message: EmailMessage;
9
+ readonly at: number;
10
+ /** Whether this call added the message to `accepted`. */
11
+ readonly taken: boolean;
12
+ readonly answer: 'accepted' | EmailDeliveryErrorCode;
13
+ }
14
+ export type MemoryRefusal = 'provider_rejected' | 'provider_unauthorized' | 'rate_limited' | 'quota_exceeded';
15
+ export interface MemoryEmailSenderOptions {
16
+ readonly capabilities?: Partial<EmailSenderCapabilities>;
17
+ readonly provider?: string;
18
+ readonly now?: () => number;
19
+ }
20
+ type MemoryHook = (message: EmailMessage, signal: AbortSignal) => Promise<void>;
21
+ export declare class MemoryEmailSender implements EmailSender {
22
+ #private;
23
+ readonly capabilities: EmailSenderCapabilities;
24
+ constructor(options?: MemoryEmailSenderOptions);
25
+ /** What reached the inbox, oldest first. */
26
+ get accepted(): readonly MemoryEmail[];
27
+ /** Every call that passed validation and the abort check. */
28
+ get handoffs(): readonly MemoryHandoff[];
29
+ /** Every following call is refused with `code` and takes nothing. */
30
+ refuse(code?: MemoryRefusal): void;
31
+ /** Every following call answers `provider_unavailable` and takes nothing. */
32
+ unavailable(): void;
33
+ /** Every following call answers `network_error` and takes nothing. */
34
+ unreachable(): void;
35
+ /** The next `count` calls take the message, then answer `network_error`. */
36
+ loseAnswers(count: number): void;
37
+ /** Runs after the handoff is recorded and before the answer; a rejection becomes `aborted` once the signal aborted, else `network_error`. */
38
+ during(hook: MemoryHook | null): void;
39
+ heal(): void;
40
+ send(message: EmailMessage, signal: AbortSignal): Promise<EmailDeliveryResult>;
41
+ }
42
+ export {};
@@ -0,0 +1,63 @@
1
+ import { type EmailDeliveryErrorCode, type EmailMessage, type EmailSenderCapabilities } from './email.js';
2
+ /** The relay's path under the receiver's URL (`FABRIKA_PLATFORM_MAIL_URL`). */
3
+ export declare const PLATFORM_MAIL_SEND_PATH = "/platform/mail/send";
4
+ /** What the relay promises: it records the message under its key and dispatches it later, perhaps after a person releases it. */
5
+ export declare const PLATFORM_MAIL_CAPABILITIES: EmailSenderCapabilities;
6
+ /**
7
+ * The request body as JSON text. It is canonical: two messages with the same content encode to the same text, so the
8
+ * relay fingerprints and stores this form.
9
+ */
10
+ export declare function encodePlatformMailRequest(message: EmailMessage): string;
11
+ /** The SHA-256 (hex) of the canonical request body: what tells a repeated send from a different one under the same key. */
12
+ export declare function platformMailFingerprint(message: EmailMessage): Promise<string>;
13
+ /** The SHA-256 (hex) of a transport key: the only form the receiver stores. */
14
+ export declare function hashPlatformKey(key: string): Promise<string>;
15
+ /**
16
+ * The key an installation service (IAM) sends to the relay with: derived from the installation secret both it and
17
+ * Control already hold, so an existing installation needs no new secret.
18
+ */
19
+ export declare function installationMailKey(installationSecret: string): Promise<string>;
20
+ export type PlatformMailRequestResult = {
21
+ readonly ok: true;
22
+ readonly message: EmailMessage;
23
+ }
24
+ /** `reason` is a fixed text that names no recipient, subject or body. */
25
+ | {
26
+ readonly ok: false;
27
+ readonly code: 'invalid_message';
28
+ readonly reason: string;
29
+ };
30
+ /** The relay's one parser of a request body (parsed JSON): a message the package's own validation accepts, or a refusal. */
31
+ export declare function parsePlatformMailRequest(body: unknown): PlatformMailRequestResult;
32
+ /** The HTTP status of each error the relay answers. */
33
+ export declare const PLATFORM_MAIL_ERROR_STATUS: {
34
+ readonly invalid_message: 400;
35
+ readonly provider_unauthorized: 401;
36
+ readonly idempotency_conflict: 409;
37
+ readonly mail_not_configured: 503;
38
+ readonly provider_unavailable: 503;
39
+ };
40
+ export type PlatformMailErrorCode = keyof typeof PLATFORM_MAIL_ERROR_STATUS;
41
+ export interface PlatformMailAccepted {
42
+ readonly status: 'accepted';
43
+ readonly messageId: string;
44
+ }
45
+ /** The relay's answer as a sender reads it: any `EmailDeliveryErrorCode`, so its `retryable` and `outcome` carry over. */
46
+ export type PlatformMailAnswer = PlatformMailAccepted | {
47
+ readonly error: {
48
+ readonly code: EmailDeliveryErrorCode;
49
+ };
50
+ };
51
+ /** The relay's answer as it writes one: an acceptance, or an error that has a status. */
52
+ export type PlatformMailReply = PlatformMailAccepted | {
53
+ readonly error: {
54
+ readonly code: PlatformMailErrorCode;
55
+ };
56
+ };
57
+ /** `200 { status: 'accepted', messageId }`, or the error's status with `{ error: { code } }`. */
58
+ export declare function platformMailAnswerResponse(reply: PlatformMailReply): Response;
59
+ /**
60
+ * The relay's answer from its status and parsed JSON body, or `null` when it is no answer the relay writes: an
61
+ * acceptance under any status but 200, an error code under a success status, or anything else.
62
+ */
63
+ export declare function parsePlatformMailAnswer(status: number, body: unknown): PlatformMailAnswer | null;
@@ -0,0 +1,19 @@
1
+ import { type EmailDeliveryResult, type EmailFetch, type EmailMessage, type EmailSender, type EmailSenderCapabilities } from './email.js';
2
+ export { encodePlatformMailRequest, hashPlatformKey, installationMailKey, parsePlatformMailAnswer, parsePlatformMailRequest, PLATFORM_MAIL_CAPABILITIES, PLATFORM_MAIL_ERROR_STATUS, PLATFORM_MAIL_SEND_PATH, platformMailAnswerResponse, platformMailFingerprint, } from './platform-wire.js';
3
+ export type { PlatformMailAccepted, PlatformMailAnswer, PlatformMailErrorCode, PlatformMailReply, PlatformMailRequestResult, } from './platform-wire.js';
4
+ /** `FABRIKA_EMAIL_PROVIDER=platform`: the application sends through the platform's relay, not a provider of its own. */
5
+ export declare const PLATFORM_EMAIL_PROVIDER = "platform";
6
+ export interface PlatformEmailSenderOptions {
7
+ /** The receiver's http(s) origin (`FABRIKA_PLATFORM_MAIL_URL`); the send path is appended to it. */
8
+ readonly url: string;
9
+ readonly key: string;
10
+ readonly fetch?: EmailFetch;
11
+ readonly timeoutMs?: number;
12
+ }
13
+ /** Hands each message to the platform's relay, which records it under its key and dispatches it later. */
14
+ export declare class PlatformEmailSender implements EmailSender {
15
+ #private;
16
+ readonly capabilities: EmailSenderCapabilities;
17
+ constructor(options: PlatformEmailSenderOptions);
18
+ send(message: EmailMessage, signal: AbortSignal): Promise<EmailDeliveryResult>;
19
+ }
@@ -0,0 +1,5 @@
1
+ /** The providers `FABRIKA_EMAIL_PROVIDER` can name in a stock installation. Memory and Directory are composition inputs. */
2
+ export declare const builtinEmailProviders: {
3
+ resend: import("./config.js").EmailProvider;
4
+ smartemailing: import("./config.js").EmailProvider;
5
+ };
@@ -0,0 +1,15 @@
1
+ import { type EmailProvider, type EmailSenderIdentity } from './config.js';
2
+ import { type EmailDeliveryResult, type EmailFetch, type EmailMessage, type EmailSender, type EmailSenderCapabilities } from './email.js';
3
+ export interface ResendEmailSenderOptions {
4
+ readonly apiKey: string;
5
+ readonly from: EmailSenderIdentity;
6
+ readonly fetch?: EmailFetch;
7
+ readonly timeoutMs?: number;
8
+ }
9
+ export declare class ResendEmailSender implements EmailSender {
10
+ #private;
11
+ readonly capabilities: EmailSenderCapabilities;
12
+ constructor(options: ResendEmailSenderOptions);
13
+ send(message: EmailMessage, signal: AbortSignal): Promise<EmailDeliveryResult>;
14
+ }
15
+ export declare const resendEmailProvider: EmailProvider;
@@ -0,0 +1,20 @@
1
+ import { type EmailProvider, type EmailSenderIdentity } from './config.js';
2
+ import { type EmailDeliveryResult, type EmailFetch, type EmailMessage, type EmailSender, type EmailSenderCapabilities } from './email.js';
3
+ export interface SmartEmailingEmailSenderOptions {
4
+ /** The account's user name or e-mail: the first half of the Basic credential. */
5
+ readonly username: string;
6
+ readonly apiKey: string;
7
+ readonly from: EmailSenderIdentity;
8
+ /** Groups the messages in SmartEmailing's statistics. */
9
+ readonly tag?: string;
10
+ readonly fetch?: EmailFetch;
11
+ readonly timeoutMs?: number;
12
+ }
13
+ export declare class SmartEmailingEmailSender implements EmailSender {
14
+ #private;
15
+ /** The API takes up to five tasks a request, but `send` carries one. */
16
+ readonly capabilities: EmailSenderCapabilities;
17
+ constructor(options: SmartEmailingEmailSenderOptions);
18
+ send(message: EmailMessage, signal: AbortSignal): Promise<EmailDeliveryResult>;
19
+ }
20
+ export declare const smartEmailingEmailProvider: EmailProvider;
@@ -0,0 +1,30 @@
1
+ import type { EmailSenderIdentity } from './config.js';
2
+ import { type EmailMessage, type EmailSenderCapabilities } from './email.js';
3
+ /** The longest subject a message may have, in UTF-16 code units: RFC 5322's line limit. */
4
+ export declare const MAX_EMAIL_SUBJECT_LENGTH = 998;
5
+ export declare function isValidEmailAddress(value: string): boolean;
6
+ /** A mailbox address, or the legacy `Name <address>` form. */
7
+ export declare function isValidEmailFrom(value: string): boolean;
8
+ /**
9
+ * Splits a bare address or the legacy `Name <address>` form. A name in one pair of double quotes loses them, since a
10
+ * transport quotes the name itself when it has to.
11
+ */
12
+ export declare function parseEmailFrom(value: string): EmailSenderIdentity | null;
13
+ /** 1..128 characters, no control characters and none of `<>"\`, so a transport can always quote it. */
14
+ export declare function isValidEmailDisplayName(value: string): boolean;
15
+ export declare function isValidEmailSenderIdentity(identity: EmailSenderIdentity): boolean;
16
+ export declare function validateEmailMessage(message: EmailMessage): void;
17
+ /**
18
+ * Refuses a message whose attachments `capabilities` does not carry: any attachment where the transport sends none, or
19
+ * more bytes together than its `maxAttachmentBytes`. `label` names the transport in the message.
20
+ */
21
+ export declare function validateEmailAttachments(message: EmailMessage, capabilities: EmailSenderCapabilities, label: string): void;
22
+ /** The most files one message carries. */
23
+ export declare const MAX_EMAIL_ATTACHMENTS = 20;
24
+ /** 1..255 characters, no control characters, no path separator and no `"`, so every transport can quote it. */
25
+ export declare function isValidAttachmentFilename(value: string): boolean;
26
+ /** The longest unsubscribe URL a header carries: one header line, with room for its name. */
27
+ export declare const MAX_UNSUBSCRIBE_URL_LENGTH = 2048;
28
+ /** An absolute `https:` URL as it is written, which a `<…>` header value carries unchanged. */
29
+ export declare function isUnsubscribeUrl(value: string): boolean;
30
+ export declare function isVisibleAscii(value: string): boolean;