@objectstack/plugin-email 4.0.1

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.
@@ -0,0 +1,256 @@
1
+ import { Plugin, PluginContext } from '@objectstack/core';
2
+ import { IEmailTransport, EmailAddress, NormalizedEmailMessage, TransportSendResult, SendEmailInput } from '@objectstack/spec/contracts';
3
+ import { EmailTemplateDefinition } from '@objectstack/spec/system';
4
+
5
+ /**
6
+ * Plugin configuration.
7
+ */
8
+ interface EmailServicePluginOptions {
9
+ /**
10
+ * Pluggable delivery transport. When omitted the plugin builds one
11
+ * from `provider`/`apiKey`; if both omitted, falls back to
12
+ * `LogTransport` (no real send).
13
+ */
14
+ transport?: IEmailTransport;
15
+ /** Provider tag — `'log' | 'resend' | 'postmark'`. Default `'log'`. */
16
+ provider?: 'log' | 'resend' | 'postmark';
17
+ /** API key for resend/postmark. */
18
+ apiKey?: string;
19
+ /** Provider-specific extra options (e.g. Postmark messageStream). */
20
+ providerOptions?: Record<string, unknown>;
21
+ /** Default `From` address applied when `input.from` is omitted. */
22
+ defaultFrom?: EmailAddress;
23
+ /** Persist each attempt to sys_email. Default true when ObjectQL engine present. */
24
+ persist?: boolean;
25
+ /** Retry attempts on transport throw. Default 0. */
26
+ retries?: number;
27
+ /** Default template render context (merged into every sendTemplate call). */
28
+ defaultTemplateContext?: Record<string, unknown>;
29
+ /** Seed built-in auth templates into sys_email_template on startup. Default true. */
30
+ seedTemplates?: boolean;
31
+ /** Additional templates seeded alongside the built-ins. */
32
+ templates?: EmailTemplateDefinition[];
33
+ }
34
+ /**
35
+ * EmailServicePlugin — registers the `email` service.
36
+ *
37
+ * Lifecycle:
38
+ * - `init`: register sys_email + sys_email_template via manifest;
39
+ * build transport (config → provider+apiKey → LogTransport fallback);
40
+ * register a transport-only EmailService so dependents can resolve it.
41
+ * - `start` (kernel:ready): wire ObjectQL-backed sys_email persistence
42
+ * + sys_email_template TemplateLoader; seed built-in auth templates
43
+ * (upsert by `(name, locale)`).
44
+ */
45
+ declare class EmailServicePlugin implements Plugin {
46
+ name: string;
47
+ version: string;
48
+ type: string;
49
+ dependencies: string[];
50
+ private readonly options;
51
+ private service?;
52
+ constructor(options?: EmailServicePluginOptions);
53
+ private resolveTransport;
54
+ init(ctx: PluginContext): Promise<void>;
55
+ start(ctx: PluginContext): Promise<void>;
56
+ /**
57
+ * Translate the `mail` settings namespace snapshot into a transport
58
+ * and `defaultFrom`, then hot-swap them on the running EmailService.
59
+ *
60
+ * Behaviour:
61
+ * - `provider = 'log' | 'smtp'` keeps the LogTransport (real SMTP
62
+ * delivery requires `@objectstack/plugin-mail-smtp`, which is not
63
+ * a dependency of this package). The from-address is still applied.
64
+ * - `provider = 'resend' | 'postmark'` rebuilds the transport using
65
+ * `api_key` from settings. If `api_key` is missing the swap is
66
+ * skipped and a warning is logged — the previous transport stays.
67
+ *
68
+ * Env-locked fields (handled in SettingsService.get) still resolve
69
+ * before this method ever sees them, so an env override transparently
70
+ * wins.
71
+ */
72
+ private applyMailSettings;
73
+ private upsertTemplate;
74
+ }
75
+
76
+ /**
77
+ * Internal persistence shim — typed loosely so the service can run
78
+ * without an ObjectQL engine wired (e.g. unit tests, serverless).
79
+ */
80
+ interface EmailPersistence {
81
+ insert(row: Record<string, any>): Promise<{
82
+ id: string;
83
+ } | string>;
84
+ update?(id: string, patch: Record<string, any>): Promise<void>;
85
+ }
86
+ /**
87
+ * Format an EmailAddress (string or {name,address}) into the canonical
88
+ * `"Display" <addr>` form. Throws if address is malformed.
89
+ */
90
+ declare function formatAddress(addr: EmailAddress): string;
91
+ /**
92
+ * Validate input + apply default-from + canonicalize recipients.
93
+ * Throws Error('VALIDATION_FAILED: <reason>') for malformed payloads.
94
+ */
95
+ declare function normalizeMessage(input: SendEmailInput, defaultFrom?: EmailAddress): NormalizedEmailMessage;
96
+ /**
97
+ * Development transport — never actually sends. Logs to the provided
98
+ * logger and returns a synthetic Message-ID. Useful for local dev,
99
+ * tests, and "dry run" environments.
100
+ */
101
+ declare class LogTransport implements IEmailTransport {
102
+ private readonly logger?;
103
+ private counter;
104
+ constructor(logger?: {
105
+ info: (msg: string, meta?: any) => void;
106
+ } | undefined);
107
+ send(message: NormalizedEmailMessage): Promise<TransportSendResult>;
108
+ }
109
+ /**
110
+ * Loader for sys_email_template rows. Injected by EmailServicePlugin
111
+ * on `kernel:ready`. Returns the best-matching row for `(name, locale)`
112
+ * or `null` when none exists / inactive.
113
+ */
114
+ interface TemplateLoader {
115
+ load(name: string, locale: string | undefined): Promise<EmailTemplateRow | null>;
116
+ }
117
+ /**
118
+ * Row shape returned by the loader — mirrors sys_email_template
119
+ * columns relevant to rendering.
120
+ */
121
+ interface EmailTemplateRow {
122
+ name: string;
123
+ locale: string;
124
+ subject: string;
125
+ body_html: string;
126
+ body_text?: string | null;
127
+ from_name?: string | null;
128
+ from_address?: string | null;
129
+ reply_to?: string | null;
130
+ active?: boolean;
131
+ variables_json?: string | null;
132
+ }
133
+ interface EmailServiceOptions {
134
+ transport: IEmailTransport;
135
+ defaultFrom?: EmailAddress;
136
+ /** Persist each attempt to sys_email. Omit to disable persistence. */
137
+ persistence?: EmailPersistence;
138
+ /** Resolve named templates for sendTemplate(). Omit to disable templates. */
139
+ templateLoader?: TemplateLoader;
140
+ /** Retry attempts on transport throw. Default 0 (no retry). */
141
+ retries?: number;
142
+ /** Logger for diagnostic output. */
143
+ logger?: {
144
+ info: (msg: string, meta?: any) => void;
145
+ warn: (msg: string, meta?: any) => void;
146
+ error?: (msg: string, meta?: any) => void;
147
+ };
148
+ /** Default render context merged into every sendTemplate call (e.g. `{ appName }`). */
149
+ defaultTemplateContext?: Record<string, unknown>;
150
+ }
151
+
152
+ /**
153
+ * Render `template` with values from `data`. Missing placeholders
154
+ * render as empty strings (no throw); call `requireVars()` first if
155
+ * you need strict validation.
156
+ */
157
+ declare function renderTemplate(template: string, data: Record<string, any>): string;
158
+ /**
159
+ * Throw `Error('MISSING_VARIABLES: a, b')` when required vars are
160
+ * absent from `data`. Used by `IEmailService.sendTemplate()` to
161
+ * fail fast rather than send a half-rendered email.
162
+ */
163
+ declare function requireVars(data: Record<string, any>, required: ReadonlyArray<string>): void;
164
+ /**
165
+ * Strip HTML tags + collapse whitespace to derive a plain-text body
166
+ * from an HTML template. Conservative: keeps line breaks at block
167
+ * boundaries (<br>, </p>, </div>) so the resulting text is at least
168
+ * paragraph-shaped.
169
+ */
170
+ declare function htmlToText(html: string): string;
171
+
172
+ /**
173
+ * ResendTransport — SaaS delivery via https://resend.com
174
+ *
175
+ * Implements `IEmailTransport` using the Resend HTTPS API. Zero
176
+ * external dependencies (uses fetch). API docs:
177
+ * https://resend.com/docs/api-reference/emails/send-email
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * new EmailServicePlugin({
182
+ * transport: new ResendTransport(process.env.RESEND_API_KEY!),
183
+ * defaultFrom: { name: 'Acme', address: 'no-reply@acme.com' },
184
+ * });
185
+ * ```
186
+ */
187
+ interface ResendTransportOptions {
188
+ apiKey: string;
189
+ /** Override the API host (used by tests / proxies). */
190
+ endpoint?: string;
191
+ }
192
+ declare class ResendTransport implements IEmailTransport {
193
+ private readonly apiKey;
194
+ private readonly endpoint;
195
+ constructor(apiKeyOrOptions: string | ResendTransportOptions);
196
+ send(message: NormalizedEmailMessage): Promise<TransportSendResult>;
197
+ }
198
+
199
+ /**
200
+ * PostmarkTransport — SaaS delivery via https://postmarkapp.com
201
+ *
202
+ * Implements `IEmailTransport` using the Postmark HTTPS API. Zero
203
+ * external dependencies (uses fetch). API docs:
204
+ * https://postmarkapp.com/developer/user-guide/send-email-with-api
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * new EmailServicePlugin({
209
+ * transport: new PostmarkTransport({
210
+ * apiKey: process.env.POSTMARK_TOKEN!,
211
+ * messageStream: 'outbound',
212
+ * }),
213
+ * defaultFrom: { name: 'Acme', address: 'no-reply@acme.com' },
214
+ * });
215
+ * ```
216
+ */
217
+ interface PostmarkTransportOptions {
218
+ apiKey: string;
219
+ /** Postmark message stream (default 'outbound'). */
220
+ messageStream?: string;
221
+ /** Override the API host. */
222
+ endpoint?: string;
223
+ }
224
+ declare class PostmarkTransport implements IEmailTransport {
225
+ private readonly apiKey;
226
+ private readonly endpoint;
227
+ private readonly messageStream;
228
+ constructor(opts: PostmarkTransportOptions);
229
+ send(message: NormalizedEmailMessage): Promise<TransportSendResult>;
230
+ }
231
+
232
+ interface MakeTransportOptions {
233
+ provider: 'log' | 'resend' | 'postmark';
234
+ apiKey?: string;
235
+ options?: Record<string, unknown>;
236
+ logger?: {
237
+ info: (msg: string, meta?: any) => void;
238
+ };
239
+ }
240
+ /**
241
+ * Build an IEmailTransport from a provider tag + opts. Used by
242
+ * EmailServicePlugin to materialise the transport selected by
243
+ * `EmailServiceConfig.provider`.
244
+ *
245
+ * Throws when a non-`log` provider is requested without an `apiKey`.
246
+ */
247
+ declare function makeTransport(opts: MakeTransportOptions): IEmailTransport;
248
+
249
+ declare const AUTH_PASSWORD_RESET_TEMPLATE: EmailTemplateDefinition;
250
+ declare const AUTH_VERIFY_EMAIL_TEMPLATE: EmailTemplateDefinition;
251
+ declare const AUTH_MAGIC_LINK_TEMPLATE: EmailTemplateDefinition;
252
+ declare const AUTH_INVITATION_TEMPLATE: EmailTemplateDefinition;
253
+ declare const AUTH_TWO_FACTOR_OTP_TEMPLATE: EmailTemplateDefinition;
254
+ declare const BUILTIN_AUTH_TEMPLATES: EmailTemplateDefinition[];
255
+
256
+ export { AUTH_INVITATION_TEMPLATE, AUTH_MAGIC_LINK_TEMPLATE, AUTH_PASSWORD_RESET_TEMPLATE, AUTH_TWO_FACTOR_OTP_TEMPLATE, AUTH_VERIFY_EMAIL_TEMPLATE, BUILTIN_AUTH_TEMPLATES, type EmailPersistence, type EmailServiceOptions, EmailServicePlugin, type EmailServicePluginOptions, type EmailTemplateRow, LogTransport, type MakeTransportOptions, PostmarkTransport, type PostmarkTransportOptions, ResendTransport, type ResendTransportOptions, type TemplateLoader, formatAddress, htmlToText, makeTransport, normalizeMessage, renderTemplate, requireVars };