@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.
- package/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +202 -0
- package/dist/index.d.mts +256 -0
- package/dist/index.d.ts +256 -0
- package/dist/index.js +877 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +835 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -0
- package/src/email-plugin.ts +361 -0
- package/src/email-service.test.ts +142 -0
- package/src/email-service.ts +366 -0
- package/src/index.ts +32 -0
- package/src/send-template.test.ts +273 -0
- package/src/template-engine.test.ts +76 -0
- package/src/template-engine.ts +94 -0
- package/src/templates/auth-templates.ts +177 -0
- package/src/transports/index.ts +39 -0
- package/src/transports/postmark.ts +94 -0
- package/src/transports/resend.ts +89 -0
- package/tsconfig.json +10 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|