@holo-js/mail 0.1.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/dist/chunk-UB6ANQ2F.mjs +552 -0
- package/dist/contracts.d.ts +320 -0
- package/dist/contracts.mjs +36 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.mjs +1321 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachContent,
|
|
3
|
+
attachFromPath,
|
|
4
|
+
attachFromStorage,
|
|
5
|
+
createAttachmentMetadata,
|
|
6
|
+
createAttachmentResolutionPlan,
|
|
7
|
+
createAttachmentResolutionPlans,
|
|
8
|
+
defineMail,
|
|
9
|
+
inferMimeTypeFromName,
|
|
10
|
+
isAttachmentQueueSafe,
|
|
11
|
+
isMailDefinition,
|
|
12
|
+
mailInternals,
|
|
13
|
+
mergeMailDefinitionInputs,
|
|
14
|
+
normalizeMailDefinition,
|
|
15
|
+
resolveAttachmentDefinition,
|
|
16
|
+
resolveNormalizedAttachment
|
|
17
|
+
} from "./chunk-UB6ANQ2F.mjs";
|
|
18
|
+
|
|
19
|
+
// src/runtime.ts
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import {
|
|
24
|
+
holoMailDefaults,
|
|
25
|
+
normalizeAppEnv
|
|
26
|
+
} from "@holo-js/config";
|
|
27
|
+
|
|
28
|
+
// src/registry.ts
|
|
29
|
+
function normalizeDriverName(value) {
|
|
30
|
+
const normalized = value.trim();
|
|
31
|
+
if (!normalized) {
|
|
32
|
+
throw new Error("[@holo-js/mail] Mail driver names must be non-empty strings.");
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
function getRegistry() {
|
|
37
|
+
const runtime = globalThis;
|
|
38
|
+
runtime.__holoMailDriverRegistry__ ??= /* @__PURE__ */ new Map();
|
|
39
|
+
return runtime.__holoMailDriverRegistry__;
|
|
40
|
+
}
|
|
41
|
+
function registerMailDriver(name, driver, options = {}) {
|
|
42
|
+
const normalizedName = normalizeDriverName(name);
|
|
43
|
+
if (typeof driver?.send !== "function") {
|
|
44
|
+
throw new Error(`[@holo-js/mail] Mail driver "${normalizedName}" must define send().`);
|
|
45
|
+
}
|
|
46
|
+
const registry = getRegistry();
|
|
47
|
+
if (registry.has(normalizedName) && options.replaceExisting !== true) {
|
|
48
|
+
throw new Error(`[@holo-js/mail] Mail driver "${normalizedName}" is already registered.`);
|
|
49
|
+
}
|
|
50
|
+
registry.set(normalizedName, Object.freeze({
|
|
51
|
+
name: normalizedName,
|
|
52
|
+
driver
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function getRegisteredMailDriver(name) {
|
|
56
|
+
return getRegistry().get(normalizeDriverName(name));
|
|
57
|
+
}
|
|
58
|
+
function listRegisteredMailDrivers() {
|
|
59
|
+
return Object.freeze([...getRegistry().values()]);
|
|
60
|
+
}
|
|
61
|
+
function resetMailDriverRegistry() {
|
|
62
|
+
getRegistry().clear();
|
|
63
|
+
}
|
|
64
|
+
var mailRegistryInternals = {
|
|
65
|
+
getRegistry,
|
|
66
|
+
normalizeDriverName
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/runtime.ts
|
|
70
|
+
var HOLO_MAIL_DELIVER_JOB = "holo.mail.deliver";
|
|
71
|
+
var MailError = class extends Error {
|
|
72
|
+
code;
|
|
73
|
+
constructor(message, code = "MAIL_ERROR", options) {
|
|
74
|
+
super(message, options);
|
|
75
|
+
this.name = "MailError";
|
|
76
|
+
this.code = code;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var MailPreviewDisabledError = class extends MailError {
|
|
80
|
+
policy;
|
|
81
|
+
constructor(policy) {
|
|
82
|
+
super(
|
|
83
|
+
`[@holo-js/mail] Mail preview is disabled for the "${policy.environment}" environment.`,
|
|
84
|
+
"MAIL_PREVIEW_DISABLED"
|
|
85
|
+
);
|
|
86
|
+
this.name = "MailPreviewDisabledError";
|
|
87
|
+
this.policy = policy;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var MailPreviewFormatUnavailableError = class extends MailError {
|
|
91
|
+
format;
|
|
92
|
+
constructor(format) {
|
|
93
|
+
super(
|
|
94
|
+
`[@holo-js/mail] Mail ${format} preview is unavailable for this message.`,
|
|
95
|
+
"MAIL_PREVIEW_FORMAT_UNAVAILABLE"
|
|
96
|
+
);
|
|
97
|
+
this.name = "MailPreviewFormatUnavailableError";
|
|
98
|
+
this.format = format;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var MailSendError = class extends MailError {
|
|
102
|
+
messageId;
|
|
103
|
+
mailer;
|
|
104
|
+
driver;
|
|
105
|
+
constructor(details, options) {
|
|
106
|
+
super(
|
|
107
|
+
details.message ?? `[@holo-js/mail] Mail delivery failed for mailer "${details.mailer}" using driver "${details.driver}".`,
|
|
108
|
+
"MAIL_SEND_FAILED",
|
|
109
|
+
options
|
|
110
|
+
);
|
|
111
|
+
this.name = "MailSendError";
|
|
112
|
+
this.messageId = details.messageId;
|
|
113
|
+
this.mailer = details.mailer;
|
|
114
|
+
this.driver = details.driver;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function getRuntimeState() {
|
|
118
|
+
const runtime = globalThis;
|
|
119
|
+
runtime.__holoMailRuntime__ ??= {};
|
|
120
|
+
return runtime.__holoMailRuntime__;
|
|
121
|
+
}
|
|
122
|
+
function getRuntimeBindings() {
|
|
123
|
+
return getRuntimeState().bindings ?? {};
|
|
124
|
+
}
|
|
125
|
+
function dynamicImport(specifier) {
|
|
126
|
+
return import(
|
|
127
|
+
/* webpackIgnore: true */
|
|
128
|
+
specifier
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
async function loadQueueModule() {
|
|
132
|
+
const override = getRuntimeState().loadQueueModule;
|
|
133
|
+
if (override) {
|
|
134
|
+
try {
|
|
135
|
+
return await override();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
138
|
+
throw new MailError(
|
|
139
|
+
"[@holo-js/mail] Queued or delayed mail delivery requires @holo-js/queue to be installed.",
|
|
140
|
+
"MAIL_QUEUE_MODULE_MISSING"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return await dynamicImport("@holo-js/queue");
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
150
|
+
throw new MailError(
|
|
151
|
+
"[@holo-js/mail] Queued or delayed mail delivery requires @holo-js/queue to be installed.",
|
|
152
|
+
"MAIL_QUEUE_MODULE_MISSING"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function loadDbModule() {
|
|
159
|
+
const override = getRuntimeState().loadDbModule;
|
|
160
|
+
if (override) {
|
|
161
|
+
try {
|
|
162
|
+
return await override();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return await dynamicImport("@holo-js/db");
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function resolveNodemailerModule(module) {
|
|
180
|
+
if (module && typeof module === "object" && "createTransport" in module && typeof module.createTransport === "function") {
|
|
181
|
+
return module;
|
|
182
|
+
}
|
|
183
|
+
if (module && typeof module === "object" && "default" in module && module.default && typeof module.default?.createTransport === "function") {
|
|
184
|
+
return module.default;
|
|
185
|
+
}
|
|
186
|
+
throw new MailError(
|
|
187
|
+
"[@holo-js/mail] Nodemailer could not be loaded for SMTP delivery.",
|
|
188
|
+
"MAIL_SMTP_MODULE_INVALID"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
async function loadNodemailerModule() {
|
|
192
|
+
const override = getRuntimeState().loadNodemailerModule;
|
|
193
|
+
if (override) {
|
|
194
|
+
try {
|
|
195
|
+
return resolveNodemailerModule(await override());
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
198
|
+
throw new MailError(
|
|
199
|
+
"[@holo-js/mail] SMTP delivery requires nodemailer to be installed.",
|
|
200
|
+
"MAIL_SMTP_MODULE_MISSING",
|
|
201
|
+
{ cause: error }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return resolveNodemailerModule(await dynamicImport("nodemailer"));
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
211
|
+
throw new MailError(
|
|
212
|
+
"[@holo-js/mail] SMTP delivery requires nodemailer to be installed.",
|
|
213
|
+
"MAIL_SMTP_MODULE_MISSING",
|
|
214
|
+
{ cause: error }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function resolveStorageModule(module) {
|
|
221
|
+
if (module && typeof module === "object" && "Storage" in module && module.Storage && typeof module.Storage?.disk === "function") {
|
|
222
|
+
return module;
|
|
223
|
+
}
|
|
224
|
+
throw new MailError(
|
|
225
|
+
"[@holo-js/mail] The storage runtime could not be loaded for storage-backed attachments.",
|
|
226
|
+
"MAIL_STORAGE_MODULE_INVALID"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
async function loadStorageModule() {
|
|
230
|
+
const override = getRuntimeState().loadStorageModule;
|
|
231
|
+
if (override) {
|
|
232
|
+
try {
|
|
233
|
+
return resolveStorageModule(await override());
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
236
|
+
throw new MailError(
|
|
237
|
+
"[@holo-js/mail] Storage-backed attachments require @holo-js/storage to be installed.",
|
|
238
|
+
"MAIL_STORAGE_MODULE_MISSING",
|
|
239
|
+
{ cause: error }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return resolveStorageModule(await dynamicImport("@holo-js/storage"));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
|
|
249
|
+
throw new MailError(
|
|
250
|
+
"[@holo-js/mail] Storage-backed attachments require @holo-js/storage to be installed.",
|
|
251
|
+
"MAIL_STORAGE_MODULE_MISSING",
|
|
252
|
+
{ cause: error }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function getFakeSentState() {
|
|
259
|
+
const state = getRuntimeState();
|
|
260
|
+
state.fakeSent ??= [];
|
|
261
|
+
return state.fakeSent;
|
|
262
|
+
}
|
|
263
|
+
function getPreviewArtifactState() {
|
|
264
|
+
const state = getRuntimeState();
|
|
265
|
+
state.previewArtifacts ??= [];
|
|
266
|
+
return state.previewArtifacts;
|
|
267
|
+
}
|
|
268
|
+
function normalizeExecutionString(value, label) {
|
|
269
|
+
const normalized = value.trim();
|
|
270
|
+
if (!normalized) {
|
|
271
|
+
throw new Error(`[@holo-js/mail] ${label} must be a non-empty string.`);
|
|
272
|
+
}
|
|
273
|
+
return normalized;
|
|
274
|
+
}
|
|
275
|
+
function normalizeExecutionDelay(value) {
|
|
276
|
+
if (typeof value === "number") {
|
|
277
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
278
|
+
throw new Error("[@holo-js/mail] Mail delays must be finite numbers greater than or equal to 0.");
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
283
|
+
throw new Error("[@holo-js/mail] Mail delays must use valid Date instances.");
|
|
284
|
+
}
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
function getResolvedConfig() {
|
|
288
|
+
return getRuntimeBindings().config ?? holoMailDefaults;
|
|
289
|
+
}
|
|
290
|
+
function resolveCurrentEnvironment() {
|
|
291
|
+
return normalizeAppEnv(process.env.APP_ENV ?? process.env.NODE_ENV);
|
|
292
|
+
}
|
|
293
|
+
function createPreviewPolicy(config = getResolvedConfig()) {
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
environment: resolveCurrentEnvironment(),
|
|
296
|
+
allowedEnvironments: config.preview.allowedEnvironments
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function assertPreviewEnabled(config = getResolvedConfig()) {
|
|
300
|
+
const policy = createPreviewPolicy(config);
|
|
301
|
+
if (!policy.allowedEnvironments.includes(policy.environment)) {
|
|
302
|
+
throw new MailPreviewDisabledError(policy);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function createPreviewHtml(preview) {
|
|
306
|
+
const header = [
|
|
307
|
+
`<h1>${escapeHtml(preview.subject)}</h1>`,
|
|
308
|
+
`<p><strong>From:</strong> ${formatAddress(preview.from)}</p>`,
|
|
309
|
+
`<p><strong>Reply-To:</strong> ${formatAddress(preview.replyTo)}</p>`,
|
|
310
|
+
`<p><strong>To:</strong> ${preview.to.map(formatAddress).join(", ")}</p>`,
|
|
311
|
+
preview.cc.length > 0 ? `<p><strong>Cc:</strong> ${preview.cc.map(formatAddress).join(", ")}</p>` : "",
|
|
312
|
+
preview.bcc.length > 0 ? `<p><strong>Bcc:</strong> ${preview.bcc.map(formatAddress).join(", ")}</p>` : ""
|
|
313
|
+
].filter(Boolean).join("");
|
|
314
|
+
const body = preview.html ? preview.html : preview.text ? `<pre>${escapeHtml(preview.text)}</pre>` : "<p>No rendered content is available.</p>";
|
|
315
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(preview.subject)}</title></head><body>${header}${body}</body></html>`;
|
|
316
|
+
}
|
|
317
|
+
function escapeHtml(value) {
|
|
318
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
319
|
+
}
|
|
320
|
+
function formatAddress(address) {
|
|
321
|
+
return address.name ? `${escapeHtml(address.name)} <${escapeHtml(address.email)}>` : escapeHtml(address.email);
|
|
322
|
+
}
|
|
323
|
+
function renderPreviewResponse(preview, format) {
|
|
324
|
+
if (format === "json") {
|
|
325
|
+
return new Response(JSON.stringify(preview, null, 2), {
|
|
326
|
+
status: 200,
|
|
327
|
+
headers: {
|
|
328
|
+
"content-type": "application/json; charset=utf-8"
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (format === "text") {
|
|
333
|
+
if (typeof preview.text !== "string") {
|
|
334
|
+
throw new MailPreviewFormatUnavailableError(format);
|
|
335
|
+
}
|
|
336
|
+
return new Response(preview.text, {
|
|
337
|
+
status: 200,
|
|
338
|
+
headers: {
|
|
339
|
+
"content-type": "text/plain; charset=utf-8"
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return new Response(createPreviewHtml(preview), {
|
|
344
|
+
status: 200,
|
|
345
|
+
headers: {
|
|
346
|
+
"content-type": "text/html; charset=utf-8"
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function getMailerConfig(mailer, config = getResolvedConfig()) {
|
|
351
|
+
const mailerConfig = config.mailers[mailer];
|
|
352
|
+
if (!mailerConfig) {
|
|
353
|
+
throw new MailError(
|
|
354
|
+
`[@holo-js/mail] Mailer "${mailer}" is not configured. Available mailers: ${Object.keys(config.mailers).join(", ")}`,
|
|
355
|
+
"MAILER_NOT_CONFIGURED"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return mailerConfig;
|
|
359
|
+
}
|
|
360
|
+
function isPreviewMailerConfig(config) {
|
|
361
|
+
return config.driver === "preview" && typeof config.path === "string";
|
|
362
|
+
}
|
|
363
|
+
function isSmtpMailerConfig(config) {
|
|
364
|
+
return config.driver === "smtp" && typeof config.host === "string" && typeof config.port === "number" && typeof config.secure === "boolean";
|
|
365
|
+
}
|
|
366
|
+
function isQueueRequested(mail2, mailer, options, config = getResolvedConfig()) {
|
|
367
|
+
const mailerConfig = getMailerConfig(mailer, config);
|
|
368
|
+
const mailQueue = mail2.queue;
|
|
369
|
+
if (typeof options.connection === "string" || typeof options.queue === "string" || typeof options.delay !== "undefined") {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
if (mailQueue === true) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
if (mailQueue === false) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
if (typeof mailQueue === "object" && mailQueue !== null) {
|
|
379
|
+
if (typeof mailQueue.connection === "string" || typeof mailQueue.queue === "string") {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
if (typeof mailQueue.queued === "boolean") {
|
|
383
|
+
return mailQueue.queued;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return mailerConfig.queue.queued === true || typeof mailerConfig.queue.connection === "string" || typeof mailerConfig.queue.queue === "string" || config.queue.queued === true || typeof config.queue.connection === "string" || typeof config.queue.queue === "string";
|
|
387
|
+
}
|
|
388
|
+
function resolveQueuePlan(mail2, options, mailer, config = getResolvedConfig()) {
|
|
389
|
+
const mailerConfig = getMailerConfig(mailer, config);
|
|
390
|
+
const mailQueue = typeof mail2.queue === "object" && mail2.queue !== null ? mail2.queue : void 0;
|
|
391
|
+
const queued = isQueueRequested(mail2, mailer, options, config);
|
|
392
|
+
return Object.freeze({
|
|
393
|
+
queued,
|
|
394
|
+
connection: queued ? options.connection ?? mailQueue?.connection ?? mailerConfig.queue.connection ?? config.queue.connection : void 0,
|
|
395
|
+
queue: queued ? options.queue ?? mailQueue?.queue ?? mailerConfig.queue.queue ?? config.queue.queue : void 0,
|
|
396
|
+
delay: queued ? options.delay ?? mail2.delay : void 0,
|
|
397
|
+
afterCommit: options.afterCommit ?? mailQueue?.afterCommit ?? mailerConfig.queue.afterCommit ?? config.queue.afterCommit
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
async function prepareMailSend(mail2, queued) {
|
|
401
|
+
const plans = createAttachmentResolutionPlans(mail2.attachments, { queued });
|
|
402
|
+
const attachments = Object.freeze(await Promise.all(plans.map((plan) => plan.resolve())));
|
|
403
|
+
return Object.freeze({
|
|
404
|
+
attachments
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function createResolvedMail(preview, attachments) {
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
from: preview.from,
|
|
410
|
+
replyTo: preview.replyTo,
|
|
411
|
+
to: preview.to,
|
|
412
|
+
cc: preview.cc,
|
|
413
|
+
bcc: preview.bcc,
|
|
414
|
+
subject: preview.subject,
|
|
415
|
+
...typeof preview.html === "string" ? { html: preview.html } : {},
|
|
416
|
+
...typeof preview.text === "string" ? { text: preview.text } : {},
|
|
417
|
+
attachments,
|
|
418
|
+
headers: preview.headers,
|
|
419
|
+
tags: preview.tags,
|
|
420
|
+
...preview.metadata ? { metadata: preview.metadata } : {},
|
|
421
|
+
...preview.priority ? { priority: preview.priority } : {}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function createSendContext(messageId, resolvedDriver, queued, deferred) {
|
|
425
|
+
return Object.freeze({
|
|
426
|
+
messageId,
|
|
427
|
+
mailer: resolvedDriver.mailer,
|
|
428
|
+
driver: resolvedDriver.driver,
|
|
429
|
+
queued,
|
|
430
|
+
...typeof deferred === "boolean" ? { deferred } : {}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function freezeSendResult(result) {
|
|
434
|
+
return Object.freeze({
|
|
435
|
+
...result,
|
|
436
|
+
...result.provider ? { provider: Object.freeze({ ...result.provider }) } : {}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function createBaseSendResult(context, overrides = {}) {
|
|
440
|
+
return freezeSendResult({
|
|
441
|
+
messageId: context.messageId,
|
|
442
|
+
mailer: context.mailer,
|
|
443
|
+
driver: context.driver,
|
|
444
|
+
queued: context.queued,
|
|
445
|
+
...typeof context.deferred === "boolean" ? { deferred: context.deferred } : {},
|
|
446
|
+
...overrides
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function normalizeDriverResult(result, context) {
|
|
450
|
+
if (!result) {
|
|
451
|
+
return createBaseSendResult(context);
|
|
452
|
+
}
|
|
453
|
+
return freezeSendResult({
|
|
454
|
+
messageId: result.messageId ?? context.messageId,
|
|
455
|
+
mailer: result.mailer ?? context.mailer,
|
|
456
|
+
driver: result.driver ?? context.driver,
|
|
457
|
+
queued: typeof result.queued === "boolean" ? result.queued : context.queued,
|
|
458
|
+
...typeof result.deferred === "boolean" ? { deferred: result.deferred } : {},
|
|
459
|
+
...typeof result.providerMessageId === "string" ? { providerMessageId: result.providerMessageId } : {},
|
|
460
|
+
...result.provider ? { provider: result.provider } : {}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function createMailRecord(context, mail2, result) {
|
|
464
|
+
return Object.freeze({
|
|
465
|
+
messageId: context.messageId,
|
|
466
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
467
|
+
mail: mail2,
|
|
468
|
+
context,
|
|
469
|
+
result
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async function persistPreviewArtifact(artifact) {
|
|
473
|
+
const mailer = getMailerConfig(artifact.context.mailer);
|
|
474
|
+
if (!isPreviewMailerConfig(mailer)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
await mkdir(mailer.path, { recursive: true });
|
|
478
|
+
await writeFile(
|
|
479
|
+
join(mailer.path, `${artifact.messageId}.json`),
|
|
480
|
+
`${JSON.stringify(artifact, null, 2)}
|
|
481
|
+
`,
|
|
482
|
+
"utf8"
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function createSmtpAddress(address) {
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
address: address.email,
|
|
488
|
+
...typeof address.name === "string" ? { name: address.name } : {}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function createSmtpHeaders(mail2) {
|
|
492
|
+
const headers = { ...mail2.headers };
|
|
493
|
+
if (mail2.tags.length > 0) {
|
|
494
|
+
headers["X-Holo-Tags"] = mail2.tags.join(", ");
|
|
495
|
+
}
|
|
496
|
+
if (mail2.metadata) {
|
|
497
|
+
headers["X-Holo-Metadata"] = JSON.stringify(mail2.metadata);
|
|
498
|
+
}
|
|
499
|
+
if (mail2.priority === "high") {
|
|
500
|
+
headers["X-Priority"] = "1";
|
|
501
|
+
headers.Importance = "high";
|
|
502
|
+
headers.Priority = "urgent";
|
|
503
|
+
} else if (mail2.priority === "low") {
|
|
504
|
+
headers["X-Priority"] = "5";
|
|
505
|
+
headers.Importance = "low";
|
|
506
|
+
headers.Priority = "non-urgent";
|
|
507
|
+
} else if (mail2.priority === "normal") {
|
|
508
|
+
headers["X-Priority"] = "3";
|
|
509
|
+
headers.Importance = "normal";
|
|
510
|
+
headers.Priority = "normal";
|
|
511
|
+
}
|
|
512
|
+
return Object.keys(headers).length > 0 ? Object.freeze(headers) : void 0;
|
|
513
|
+
}
|
|
514
|
+
async function createSmtpAttachment(attachment) {
|
|
515
|
+
const base = {
|
|
516
|
+
filename: attachment.name,
|
|
517
|
+
...typeof attachment.contentType === "string" ? { contentType: attachment.contentType } : {},
|
|
518
|
+
contentDisposition: attachment.disposition,
|
|
519
|
+
...typeof attachment.contentId === "string" ? { cid: attachment.contentId } : {}
|
|
520
|
+
};
|
|
521
|
+
if (typeof attachment.path === "string") {
|
|
522
|
+
return Object.freeze({
|
|
523
|
+
...base,
|
|
524
|
+
path: attachment.path
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (attachment.storage) {
|
|
528
|
+
const storageModule = await loadStorageModule();
|
|
529
|
+
const disk = storageModule.Storage.disk(attachment.storage.disk);
|
|
530
|
+
if (disk.driver !== "s3") {
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
...base,
|
|
533
|
+
path: disk.path(attachment.storage.path)
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const bytes = await disk.getBytes(attachment.storage.path);
|
|
537
|
+
if (!bytes) {
|
|
538
|
+
throw new MailError(
|
|
539
|
+
`[@holo-js/mail] Storage attachment "${attachment.storage.path}" could not be read for SMTP delivery.`,
|
|
540
|
+
"MAIL_STORAGE_ATTACHMENT_MISSING"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze({
|
|
544
|
+
...base,
|
|
545
|
+
content: bytes
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (typeof attachment.content !== "undefined") {
|
|
549
|
+
return Object.freeze({
|
|
550
|
+
...base,
|
|
551
|
+
content: attachment.content
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
throw new MailError(
|
|
555
|
+
`[@holo-js/mail] Attachment "${attachment.name}" could not be translated for SMTP delivery.`,
|
|
556
|
+
"MAIL_SMTP_ATTACHMENT_INVALID"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
async function createSmtpMessage(mail2, context) {
|
|
560
|
+
const attachments = mail2.attachments.length > 0 ? Object.freeze(await Promise.all(mail2.attachments.map(createSmtpAttachment))) : void 0;
|
|
561
|
+
const headers = createSmtpHeaders(mail2);
|
|
562
|
+
return Object.freeze({
|
|
563
|
+
messageId: context.messageId,
|
|
564
|
+
from: createSmtpAddress(mail2.from),
|
|
565
|
+
replyTo: createSmtpAddress(mail2.replyTo),
|
|
566
|
+
to: Object.freeze(mail2.to.map(createSmtpAddress)),
|
|
567
|
+
...mail2.cc.length > 0 ? { cc: Object.freeze(mail2.cc.map(createSmtpAddress)) } : {},
|
|
568
|
+
...mail2.bcc.length > 0 ? { bcc: Object.freeze(mail2.bcc.map(createSmtpAddress)) } : {},
|
|
569
|
+
subject: mail2.subject,
|
|
570
|
+
...typeof mail2.html === "string" ? { html: mail2.html } : {},
|
|
571
|
+
...typeof mail2.text === "string" ? { text: mail2.text } : {},
|
|
572
|
+
...attachments ? { attachments } : {},
|
|
573
|
+
...headers ? { headers } : {}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async function sendViaSmtp(mail2, context) {
|
|
577
|
+
const nodemailer = await loadNodemailerModule();
|
|
578
|
+
const mailerConfig = getMailerConfig(context.mailer);
|
|
579
|
+
if (!isSmtpMailerConfig(mailerConfig)) {
|
|
580
|
+
throw new MailError(
|
|
581
|
+
`[@holo-js/mail] Mailer "${context.mailer}" is not configured for SMTP delivery.`,
|
|
582
|
+
"MAIL_SMTP_MAILER_INVALID"
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const transporter = nodemailer.createTransport({
|
|
586
|
+
host: mailerConfig.host,
|
|
587
|
+
port: mailerConfig.port,
|
|
588
|
+
secure: mailerConfig.secure,
|
|
589
|
+
...typeof mailerConfig.user === "string" ? {
|
|
590
|
+
auth: {
|
|
591
|
+
user: mailerConfig.user,
|
|
592
|
+
...typeof mailerConfig.password === "string" ? { pass: mailerConfig.password } : {}
|
|
593
|
+
}
|
|
594
|
+
} : {}
|
|
595
|
+
});
|
|
596
|
+
const smtpMessage = await createSmtpMessage(mail2, context);
|
|
597
|
+
const result = await transporter.sendMail(smtpMessage);
|
|
598
|
+
return createBaseSendResult(context, {
|
|
599
|
+
...typeof result.messageId === "string" ? { providerMessageId: result.messageId } : {},
|
|
600
|
+
...typeof result.response === "string" ? {
|
|
601
|
+
provider: {
|
|
602
|
+
response: result.response
|
|
603
|
+
}
|
|
604
|
+
} : {}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
var builtInDrivers = Object.freeze({
|
|
608
|
+
preview: Object.freeze({
|
|
609
|
+
async send(mail2, context) {
|
|
610
|
+
const result = createBaseSendResult(context);
|
|
611
|
+
const artifact = createMailRecord(context, mail2, result);
|
|
612
|
+
getPreviewArtifactState().push(artifact);
|
|
613
|
+
await persistPreviewArtifact(artifact);
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
}),
|
|
617
|
+
fake: Object.freeze({
|
|
618
|
+
send(mail2, context) {
|
|
619
|
+
const result = createBaseSendResult(context);
|
|
620
|
+
getFakeSentState().push(createMailRecord(context, mail2, result));
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
}),
|
|
624
|
+
log: Object.freeze({
|
|
625
|
+
send(mail2, context) {
|
|
626
|
+
const mailerConfig = getMailerConfig(context.mailer);
|
|
627
|
+
const verbose = mailerConfig.driver === "log" ? mailerConfig.logBodies : false;
|
|
628
|
+
const payload = {
|
|
629
|
+
messageId: context.messageId,
|
|
630
|
+
mailer: context.mailer,
|
|
631
|
+
driver: context.driver,
|
|
632
|
+
to: mail2.to.map((address) => address.email),
|
|
633
|
+
subject: mail2.subject,
|
|
634
|
+
hasHtml: typeof mail2.html === "string",
|
|
635
|
+
hasText: typeof mail2.text === "string",
|
|
636
|
+
attachments: mail2.attachments.map((attachment) => ({
|
|
637
|
+
name: attachment.name,
|
|
638
|
+
source: attachment.source,
|
|
639
|
+
disposition: attachment.disposition
|
|
640
|
+
})),
|
|
641
|
+
...mail2.tags.length > 0 ? { tags: [...mail2.tags] } : {},
|
|
642
|
+
...mail2.priority ? { priority: mail2.priority } : {},
|
|
643
|
+
...verbose ? {
|
|
644
|
+
html: mail2.html,
|
|
645
|
+
text: mail2.text
|
|
646
|
+
} : {}
|
|
647
|
+
};
|
|
648
|
+
console.warn("[@holo-js/mail] Logged mail send", payload);
|
|
649
|
+
return createBaseSendResult(context);
|
|
650
|
+
}
|
|
651
|
+
}),
|
|
652
|
+
smtp: Object.freeze({
|
|
653
|
+
async send(mail2, context) {
|
|
654
|
+
return await sendViaSmtp(mail2, context);
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
});
|
|
658
|
+
function resolveDriver(mail2, options, config) {
|
|
659
|
+
const selectedMailer = typeof options.mailer === "string" ? options.mailer : resolveMailerName(mail2, config);
|
|
660
|
+
const mailerConfig = getMailerConfig(selectedMailer, config);
|
|
661
|
+
const driverName = mailerConfig.driver;
|
|
662
|
+
const builtIn = builtInDrivers[driverName];
|
|
663
|
+
if (builtIn) {
|
|
664
|
+
return Object.freeze({
|
|
665
|
+
mailer: selectedMailer,
|
|
666
|
+
driver: driverName,
|
|
667
|
+
implementation: builtIn
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const registered = getRegisteredMailDriver(driverName);
|
|
671
|
+
if (!registered) {
|
|
672
|
+
throw new MailError(
|
|
673
|
+
`[@holo-js/mail] Mail driver "${driverName}" is not registered.`,
|
|
674
|
+
"MAIL_DRIVER_NOT_REGISTERED"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return Object.freeze({
|
|
678
|
+
mailer: selectedMailer,
|
|
679
|
+
driver: driverName,
|
|
680
|
+
implementation: registered.driver
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
function resolveDriverByName(mailer, driver) {
|
|
684
|
+
const builtIn = builtInDrivers[driver];
|
|
685
|
+
if (builtIn) {
|
|
686
|
+
return Object.freeze({
|
|
687
|
+
mailer,
|
|
688
|
+
driver,
|
|
689
|
+
implementation: builtIn
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
const registered = getRegisteredMailDriver(driver);
|
|
693
|
+
if (!registered) {
|
|
694
|
+
throw new MailError(
|
|
695
|
+
`[@holo-js/mail] Mail driver "${driver}" is not registered.`,
|
|
696
|
+
"MAIL_DRIVER_NOT_REGISTERED"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return Object.freeze({
|
|
700
|
+
mailer,
|
|
701
|
+
driver,
|
|
702
|
+
implementation: registered.driver
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function serializeQueuedAttachment(attachment) {
|
|
706
|
+
return Object.freeze({
|
|
707
|
+
source: attachment.source,
|
|
708
|
+
name: attachment.name,
|
|
709
|
+
...typeof attachment.contentType === "string" ? { contentType: attachment.contentType } : {},
|
|
710
|
+
disposition: attachment.disposition,
|
|
711
|
+
...typeof attachment.contentId === "string" ? { contentId: attachment.contentId } : {},
|
|
712
|
+
...typeof attachment.path === "string" ? { path: attachment.path } : {},
|
|
713
|
+
...attachment.storage ? { storage: attachment.storage } : {},
|
|
714
|
+
...typeof attachment.content === "string" ? { content: attachment.content } : attachment.content instanceof Uint8Array ? {
|
|
715
|
+
content: Object.freeze({
|
|
716
|
+
encoding: "base64",
|
|
717
|
+
value: Buffer.from(attachment.content).toString("base64")
|
|
718
|
+
})
|
|
719
|
+
} : {}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function deserializeQueuedAttachment(attachment) {
|
|
723
|
+
return Object.freeze({
|
|
724
|
+
source: attachment.source,
|
|
725
|
+
name: attachment.name,
|
|
726
|
+
...typeof attachment.contentType === "string" ? { contentType: attachment.contentType } : {},
|
|
727
|
+
disposition: attachment.disposition,
|
|
728
|
+
...typeof attachment.contentId === "string" ? { contentId: attachment.contentId } : {},
|
|
729
|
+
...typeof attachment.path === "string" ? { path: attachment.path } : {},
|
|
730
|
+
...attachment.storage ? { storage: attachment.storage } : {},
|
|
731
|
+
...typeof attachment.content === "string" ? { content: attachment.content } : attachment.content ? { content: new Uint8Array(Buffer.from(attachment.content.value, "base64")) } : {}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function createQueuedMailPayload(mail2, context) {
|
|
735
|
+
return Object.freeze({
|
|
736
|
+
messageId: context.messageId,
|
|
737
|
+
mailer: context.mailer,
|
|
738
|
+
driver: context.driver,
|
|
739
|
+
queued: true,
|
|
740
|
+
...typeof context.deferred === "boolean" ? { deferred: context.deferred } : {},
|
|
741
|
+
mail: Object.freeze({
|
|
742
|
+
from: mail2.from,
|
|
743
|
+
replyTo: mail2.replyTo,
|
|
744
|
+
to: mail2.to,
|
|
745
|
+
cc: mail2.cc,
|
|
746
|
+
bcc: mail2.bcc,
|
|
747
|
+
subject: mail2.subject,
|
|
748
|
+
...typeof mail2.html === "string" ? { html: mail2.html } : {},
|
|
749
|
+
...typeof mail2.text === "string" ? { text: mail2.text } : {},
|
|
750
|
+
attachments: Object.freeze(mail2.attachments.map(serializeQueuedAttachment)),
|
|
751
|
+
headers: mail2.headers,
|
|
752
|
+
tags: mail2.tags,
|
|
753
|
+
...mail2.metadata ? { metadata: mail2.metadata } : {},
|
|
754
|
+
...mail2.priority ? { priority: mail2.priority } : {}
|
|
755
|
+
})
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
function createResolvedMailFromQueuedPayload(payload) {
|
|
759
|
+
return Object.freeze({
|
|
760
|
+
from: payload.mail.from,
|
|
761
|
+
replyTo: payload.mail.replyTo,
|
|
762
|
+
to: payload.mail.to,
|
|
763
|
+
cc: payload.mail.cc,
|
|
764
|
+
bcc: payload.mail.bcc,
|
|
765
|
+
subject: payload.mail.subject,
|
|
766
|
+
...typeof payload.mail.html === "string" ? { html: payload.mail.html } : {},
|
|
767
|
+
...typeof payload.mail.text === "string" ? { text: payload.mail.text } : {},
|
|
768
|
+
attachments: Object.freeze(payload.mail.attachments.map(deserializeQueuedAttachment)),
|
|
769
|
+
headers: payload.mail.headers,
|
|
770
|
+
tags: payload.mail.tags,
|
|
771
|
+
...payload.mail.metadata ? { metadata: payload.mail.metadata } : {},
|
|
772
|
+
...payload.mail.priority ? { priority: payload.mail.priority } : {}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async function deliverResolvedMail(mail2, resolvedDriver, context) {
|
|
776
|
+
try {
|
|
777
|
+
const result = await resolvedDriver.implementation.send(mail2, context);
|
|
778
|
+
return normalizeDriverResult(result, context);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
throw new MailSendError({
|
|
781
|
+
messageId: context.messageId,
|
|
782
|
+
mailer: context.mailer,
|
|
783
|
+
driver: context.driver
|
|
784
|
+
}, {
|
|
785
|
+
cause: error
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function runQueuedMailDelivery(payload) {
|
|
790
|
+
const resolvedDriver = resolveDriverByName(payload.mailer, payload.driver);
|
|
791
|
+
const context = Object.freeze({
|
|
792
|
+
messageId: payload.messageId,
|
|
793
|
+
mailer: payload.mailer,
|
|
794
|
+
driver: payload.driver,
|
|
795
|
+
queued: true,
|
|
796
|
+
...typeof payload.deferred === "boolean" ? { deferred: payload.deferred } : {}
|
|
797
|
+
});
|
|
798
|
+
return await deliverResolvedMail(
|
|
799
|
+
createResolvedMailFromQueuedPayload(payload),
|
|
800
|
+
resolvedDriver,
|
|
801
|
+
context
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
async function ensureMailQueueJobRegistered(queueModule) {
|
|
805
|
+
const resolvedQueueModule = queueModule ?? await loadQueueModule();
|
|
806
|
+
if (resolvedQueueModule.getRegisteredQueueJob(HOLO_MAIL_DELIVER_JOB)) {
|
|
807
|
+
return resolvedQueueModule;
|
|
808
|
+
}
|
|
809
|
+
resolvedQueueModule.registerQueueJob(
|
|
810
|
+
resolvedQueueModule.defineJob({
|
|
811
|
+
async handle(payload) {
|
|
812
|
+
return await runQueuedMailDelivery(payload);
|
|
813
|
+
}
|
|
814
|
+
}),
|
|
815
|
+
{ name: HOLO_MAIL_DELIVER_JOB }
|
|
816
|
+
);
|
|
817
|
+
return resolvedQueueModule;
|
|
818
|
+
}
|
|
819
|
+
async function dispatchQueuedMail(mail2, context, plan) {
|
|
820
|
+
const queueModule = await ensureMailQueueJobRegistered();
|
|
821
|
+
let pending = queueModule.dispatch(
|
|
822
|
+
HOLO_MAIL_DELIVER_JOB,
|
|
823
|
+
createQueuedMailPayload(mail2, context)
|
|
824
|
+
);
|
|
825
|
+
if (typeof plan.connection !== "undefined") {
|
|
826
|
+
pending = pending.onConnection(plan.connection);
|
|
827
|
+
}
|
|
828
|
+
if (typeof plan.queue !== "undefined") {
|
|
829
|
+
pending = pending.onQueue(plan.queue);
|
|
830
|
+
}
|
|
831
|
+
if (typeof plan.delay !== "undefined") {
|
|
832
|
+
pending = pending.delay(plan.delay);
|
|
833
|
+
}
|
|
834
|
+
await pending.dispatch();
|
|
835
|
+
return createBaseSendResult(context);
|
|
836
|
+
}
|
|
837
|
+
async function deferSendUntilCommit(context, callback) {
|
|
838
|
+
const dbModule = await loadDbModule();
|
|
839
|
+
const active = dbModule?.connectionAsyncContext.getActive()?.connection;
|
|
840
|
+
if (!active || active.getScope().kind === "root") {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
active.afterCommit(async () => {
|
|
844
|
+
await callback();
|
|
845
|
+
});
|
|
846
|
+
return createBaseSendResult(context);
|
|
847
|
+
}
|
|
848
|
+
async function renderView(input) {
|
|
849
|
+
const renderer = getRuntimeBindings().renderView;
|
|
850
|
+
if (!renderer) {
|
|
851
|
+
throw new MailError(`[@holo-js/mail] Mail view rendering requires a renderView runtime binding for "${input.view}".`, "MAIL_VIEW_RENDERER_MISSING");
|
|
852
|
+
}
|
|
853
|
+
const html = await renderer(input);
|
|
854
|
+
if (typeof html !== "string" || !html.trim()) {
|
|
855
|
+
throw new MailError(`[@holo-js/mail] Mail view "${input.view}" must render a non-empty HTML string.`, "MAIL_VIEW_RENDER_FAILED");
|
|
856
|
+
}
|
|
857
|
+
return html;
|
|
858
|
+
}
|
|
859
|
+
function stripMarkdownSyntax(markdown) {
|
|
860
|
+
return markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^[-*]\s+/gm, "").trim();
|
|
861
|
+
}
|
|
862
|
+
function renderMarkdownInline(markdown) {
|
|
863
|
+
return escapeHtml(markdown).replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
864
|
+
}
|
|
865
|
+
function renderMarkdown(markdown) {
|
|
866
|
+
const blocks = markdown.trim().split(/\n\s*\n/g).map((block) => block.trim()).filter(Boolean);
|
|
867
|
+
return blocks.map((block) => {
|
|
868
|
+
const lines = block.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
869
|
+
if (lines.length === 0) {
|
|
870
|
+
return "";
|
|
871
|
+
}
|
|
872
|
+
if (lines.every((line) => /^[-*]\s+/.test(line))) {
|
|
873
|
+
return `<ul>${lines.map((line) => `<li>${renderMarkdownInline(line.replace(/^[-*]\s+/, ""))}</li>`).join("")}</ul>`;
|
|
874
|
+
}
|
|
875
|
+
const heading = lines.length === 1 ? lines[0].match(/^(#{1,6})\s+(.+)$/) : null;
|
|
876
|
+
if (heading) {
|
|
877
|
+
const [, markers, title] = heading;
|
|
878
|
+
const level = markers.length;
|
|
879
|
+
return `<h${level}>${renderMarkdownInline(title)}</h${level}>`;
|
|
880
|
+
}
|
|
881
|
+
return `<p>${lines.map(renderMarkdownInline).join("<br />")}</p>`;
|
|
882
|
+
}).join("\n");
|
|
883
|
+
}
|
|
884
|
+
function resolveSourceKind(mail2) {
|
|
885
|
+
if (mail2.render) {
|
|
886
|
+
return "render";
|
|
887
|
+
}
|
|
888
|
+
if (typeof mail2.markdown === "string") {
|
|
889
|
+
return "markdown";
|
|
890
|
+
}
|
|
891
|
+
if (typeof mail2.html === "string") {
|
|
892
|
+
return "html";
|
|
893
|
+
}
|
|
894
|
+
return "text";
|
|
895
|
+
}
|
|
896
|
+
function resolveMailerName(mail2, config) {
|
|
897
|
+
return mail2.mailer ?? config.default;
|
|
898
|
+
}
|
|
899
|
+
function resolveEnvelope(mail2) {
|
|
900
|
+
const config = getResolvedConfig();
|
|
901
|
+
const mailer = resolveMailerName(mail2, config);
|
|
902
|
+
const mailerConfig = getMailerConfig(mailer, config);
|
|
903
|
+
const from = mail2.from ?? mailerConfig.from ?? config.from;
|
|
904
|
+
if (!from) {
|
|
905
|
+
throw new MailError("[@holo-js/mail] Mail preview requires a resolvable from address.", "MAIL_FROM_MISSING");
|
|
906
|
+
}
|
|
907
|
+
const replyTo = mail2.replyTo ?? mailerConfig.replyTo ?? config.replyTo ?? from;
|
|
908
|
+
return Object.freeze({
|
|
909
|
+
config,
|
|
910
|
+
mailer,
|
|
911
|
+
from,
|
|
912
|
+
replyTo,
|
|
913
|
+
to: mail2.to,
|
|
914
|
+
cc: mail2.cc,
|
|
915
|
+
bcc: mail2.bcc
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function createMarkdownWrapperProps(mail2, envelope, html, text) {
|
|
919
|
+
const serializeAddress = (address) => Object.freeze({
|
|
920
|
+
email: address.email,
|
|
921
|
+
...address.name ? { name: address.name } : {}
|
|
922
|
+
});
|
|
923
|
+
return Object.freeze({
|
|
924
|
+
subject: mail2.subject,
|
|
925
|
+
html,
|
|
926
|
+
markdown: mail2.markdown,
|
|
927
|
+
text,
|
|
928
|
+
from: serializeAddress(envelope.from),
|
|
929
|
+
replyTo: serializeAddress(envelope.replyTo),
|
|
930
|
+
to: Object.freeze(envelope.to.map(serializeAddress)),
|
|
931
|
+
cc: Object.freeze(envelope.cc.map(serializeAddress)),
|
|
932
|
+
bcc: Object.freeze(envelope.bcc.map(serializeAddress)),
|
|
933
|
+
headers: Object.freeze({ ...mail2.headers }),
|
|
934
|
+
tags: Object.freeze([...mail2.tags]),
|
|
935
|
+
...mail2.metadata ? { metadata: mail2.metadata } : {},
|
|
936
|
+
...mail2.priority ? { priority: mail2.priority } : {}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
async function renderContent(mail2, envelope) {
|
|
940
|
+
if (mail2.render) {
|
|
941
|
+
const html = await renderView(mail2.render);
|
|
942
|
+
return Object.freeze({
|
|
943
|
+
kind: "render",
|
|
944
|
+
html,
|
|
945
|
+
...typeof mail2.text === "string" ? { text: mail2.text } : {},
|
|
946
|
+
source: Object.freeze({
|
|
947
|
+
kind: "render",
|
|
948
|
+
render: mail2.render
|
|
949
|
+
})
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (typeof mail2.markdown === "string") {
|
|
953
|
+
const baseHtml = renderMarkdown(mail2.markdown);
|
|
954
|
+
const text = mail2.text ?? stripMarkdownSyntax(mail2.markdown);
|
|
955
|
+
const wrapper = mail2.markdownWrapper ?? envelope.config.markdown.wrapper;
|
|
956
|
+
const html = wrapper ? await renderView({
|
|
957
|
+
view: wrapper,
|
|
958
|
+
props: createMarkdownWrapperProps(mail2, envelope, baseHtml, text)
|
|
959
|
+
}) : baseHtml;
|
|
960
|
+
return Object.freeze({
|
|
961
|
+
kind: "markdown",
|
|
962
|
+
html,
|
|
963
|
+
text,
|
|
964
|
+
source: Object.freeze({
|
|
965
|
+
kind: "markdown",
|
|
966
|
+
markdown: mail2.markdown
|
|
967
|
+
})
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
if (typeof mail2.html === "string") {
|
|
971
|
+
return Object.freeze({
|
|
972
|
+
kind: "html",
|
|
973
|
+
html: mail2.html,
|
|
974
|
+
...typeof mail2.text === "string" ? { text: mail2.text } : {},
|
|
975
|
+
source: Object.freeze({
|
|
976
|
+
kind: "html",
|
|
977
|
+
rawHtml: mail2.html
|
|
978
|
+
})
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return Object.freeze({
|
|
982
|
+
kind: "text",
|
|
983
|
+
text: mail2.text,
|
|
984
|
+
source: Object.freeze({
|
|
985
|
+
kind: "text"
|
|
986
|
+
})
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
async function computePreview(mail2) {
|
|
990
|
+
const envelope = resolveEnvelope(mail2);
|
|
991
|
+
const rendered = await renderContent(mail2, envelope);
|
|
992
|
+
const preview = Object.freeze({
|
|
993
|
+
from: envelope.from,
|
|
994
|
+
replyTo: envelope.replyTo,
|
|
995
|
+
to: envelope.to,
|
|
996
|
+
cc: envelope.cc,
|
|
997
|
+
bcc: envelope.bcc,
|
|
998
|
+
subject: mail2.subject,
|
|
999
|
+
...typeof rendered.html === "string" ? { html: rendered.html } : {},
|
|
1000
|
+
...typeof rendered.text === "string" ? { text: rendered.text } : {},
|
|
1001
|
+
attachments: Object.freeze(mail2.attachments.map((attachment) => createAttachmentMetadata(attachment))),
|
|
1002
|
+
headers: mail2.headers,
|
|
1003
|
+
tags: mail2.tags,
|
|
1004
|
+
...mail2.metadata ? { metadata: mail2.metadata } : {},
|
|
1005
|
+
...mail2.priority ? { priority: mail2.priority } : {},
|
|
1006
|
+
source: rendered.source
|
|
1007
|
+
});
|
|
1008
|
+
return Object.freeze({
|
|
1009
|
+
...envelope,
|
|
1010
|
+
preview
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
var PendingSend = class {
|
|
1014
|
+
#mail;
|
|
1015
|
+
#options;
|
|
1016
|
+
#promise;
|
|
1017
|
+
constructor(mail2, options = {}) {
|
|
1018
|
+
this.#mail = mail2;
|
|
1019
|
+
this.#options = options;
|
|
1020
|
+
}
|
|
1021
|
+
using(name) {
|
|
1022
|
+
this.#options.mailer = normalizeExecutionString(name, "Mail mailer");
|
|
1023
|
+
return this;
|
|
1024
|
+
}
|
|
1025
|
+
onConnection(name) {
|
|
1026
|
+
this.#options.connection = normalizeExecutionString(name, "Mail queue connection");
|
|
1027
|
+
return this;
|
|
1028
|
+
}
|
|
1029
|
+
onQueue(name) {
|
|
1030
|
+
this.#options.queue = normalizeExecutionString(name, "Mail queue name");
|
|
1031
|
+
return this;
|
|
1032
|
+
}
|
|
1033
|
+
delay(value) {
|
|
1034
|
+
this.#options.delay = normalizeExecutionDelay(value);
|
|
1035
|
+
return this;
|
|
1036
|
+
}
|
|
1037
|
+
afterCommit() {
|
|
1038
|
+
this.#options.afterCommit = true;
|
|
1039
|
+
return this;
|
|
1040
|
+
}
|
|
1041
|
+
then(onfulfilled, onrejected) {
|
|
1042
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
1043
|
+
}
|
|
1044
|
+
catch(onrejected) {
|
|
1045
|
+
return this.execute().catch(onrejected);
|
|
1046
|
+
}
|
|
1047
|
+
finally(onfinally) {
|
|
1048
|
+
return this.execute().finally(onfinally);
|
|
1049
|
+
}
|
|
1050
|
+
execute() {
|
|
1051
|
+
if (!this.#promise) {
|
|
1052
|
+
this.#promise = this.#execute();
|
|
1053
|
+
}
|
|
1054
|
+
return this.#promise;
|
|
1055
|
+
}
|
|
1056
|
+
async #execute(execution = {}) {
|
|
1057
|
+
const options = Object.freeze({ ...this.#options });
|
|
1058
|
+
const config = getResolvedConfig();
|
|
1059
|
+
const resolvedDriver = resolveDriver(this.#mail, options, config);
|
|
1060
|
+
const plan = resolveQueuePlan(this.#mail, options, resolvedDriver.mailer, config);
|
|
1061
|
+
const messageId = randomUUID();
|
|
1062
|
+
const prepared = await prepareMailSend(this.#mail, plan.queued);
|
|
1063
|
+
const input = Object.freeze({
|
|
1064
|
+
mail: this.#mail,
|
|
1065
|
+
attachments: prepared.attachments,
|
|
1066
|
+
options
|
|
1067
|
+
});
|
|
1068
|
+
const sendOverride = getRuntimeBindings().send;
|
|
1069
|
+
if (!plan.queued && plan.afterCommit !== true && sendOverride) {
|
|
1070
|
+
const context = createSendContext(messageId, resolvedDriver, false);
|
|
1071
|
+
return normalizeDriverResult(await sendOverride(input), context);
|
|
1072
|
+
}
|
|
1073
|
+
const { preview } = await computePreview(this.#mail);
|
|
1074
|
+
const resolvedMail = createResolvedMail(preview, prepared.attachments);
|
|
1075
|
+
const executionContext = createSendContext(
|
|
1076
|
+
messageId,
|
|
1077
|
+
resolvedDriver,
|
|
1078
|
+
plan.queued,
|
|
1079
|
+
plan.afterCommit ? true : void 0
|
|
1080
|
+
);
|
|
1081
|
+
if (execution.allowAfterCommitDeferral !== false && plan.afterCommit) {
|
|
1082
|
+
const deferredResult = await deferSendUntilCommit(executionContext, async () => {
|
|
1083
|
+
if (plan.queued) {
|
|
1084
|
+
return await dispatchQueuedMail(resolvedMail, executionContext, plan);
|
|
1085
|
+
}
|
|
1086
|
+
return await deliverResolvedMail(resolvedMail, resolvedDriver, executionContext);
|
|
1087
|
+
});
|
|
1088
|
+
if (deferredResult) {
|
|
1089
|
+
return deferredResult;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (plan.queued) {
|
|
1093
|
+
return await dispatchQueuedMail(
|
|
1094
|
+
resolvedMail,
|
|
1095
|
+
createSendContext(messageId, resolvedDriver, true),
|
|
1096
|
+
plan
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
return await deliverResolvedMail(
|
|
1100
|
+
resolvedMail,
|
|
1101
|
+
resolvedDriver,
|
|
1102
|
+
createSendContext(messageId, resolvedDriver, false)
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
function listFakeSentMails() {
|
|
1107
|
+
return Object.freeze([...getFakeSentState()]);
|
|
1108
|
+
}
|
|
1109
|
+
function resetFakeSentMails() {
|
|
1110
|
+
getFakeSentState().length = 0;
|
|
1111
|
+
}
|
|
1112
|
+
function listPreviewMailArtifacts() {
|
|
1113
|
+
return Object.freeze([...getPreviewArtifactState()]);
|
|
1114
|
+
}
|
|
1115
|
+
function resetPreviewMailArtifacts() {
|
|
1116
|
+
getPreviewArtifactState().length = 0;
|
|
1117
|
+
}
|
|
1118
|
+
function resolveMailInput(mail2, overrides) {
|
|
1119
|
+
if (!overrides || Object.keys(overrides).length === 0) {
|
|
1120
|
+
return normalizeMailDefinition(mail2);
|
|
1121
|
+
}
|
|
1122
|
+
return normalizeMailDefinition(mergeMailDefinitionInputs(mail2, overrides));
|
|
1123
|
+
}
|
|
1124
|
+
function configureMailRuntime(bindings) {
|
|
1125
|
+
getRuntimeState().bindings = bindings;
|
|
1126
|
+
}
|
|
1127
|
+
function getMailRuntimeBindings() {
|
|
1128
|
+
return getRuntimeBindings();
|
|
1129
|
+
}
|
|
1130
|
+
function resetMailRuntime() {
|
|
1131
|
+
const state = getRuntimeState();
|
|
1132
|
+
state.bindings = void 0;
|
|
1133
|
+
state.fakeSent = void 0;
|
|
1134
|
+
state.previewArtifacts = void 0;
|
|
1135
|
+
state.loadQueueModule = void 0;
|
|
1136
|
+
state.loadDbModule = void 0;
|
|
1137
|
+
state.loadNodemailerModule = void 0;
|
|
1138
|
+
state.loadStorageModule = void 0;
|
|
1139
|
+
}
|
|
1140
|
+
function sendMail(mail2, overrides) {
|
|
1141
|
+
return new PendingSend(resolveMailInput(mail2, overrides));
|
|
1142
|
+
}
|
|
1143
|
+
async function previewMail(mail2, overrides) {
|
|
1144
|
+
const normalizedMail = resolveMailInput(mail2, overrides);
|
|
1145
|
+
const previewHandler = getRuntimeBindings().preview;
|
|
1146
|
+
if (previewHandler) {
|
|
1147
|
+
return Promise.resolve(previewHandler({
|
|
1148
|
+
mail: normalizedMail
|
|
1149
|
+
}));
|
|
1150
|
+
}
|
|
1151
|
+
return (await computePreview(normalizedMail)).preview;
|
|
1152
|
+
}
|
|
1153
|
+
async function renderMailPreview(mail2, overrides, format = "html") {
|
|
1154
|
+
const normalizedMail = resolveMailInput(mail2, overrides);
|
|
1155
|
+
try {
|
|
1156
|
+
assertPreviewEnabled();
|
|
1157
|
+
const explicitRenderer = getRuntimeBindings().renderPreview;
|
|
1158
|
+
if (explicitRenderer) {
|
|
1159
|
+
return Promise.resolve(explicitRenderer({
|
|
1160
|
+
mail: normalizedMail,
|
|
1161
|
+
format
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
const preview = await previewMail(normalizedMail);
|
|
1165
|
+
return renderPreviewResponse(preview, format);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (error instanceof MailPreviewDisabledError) {
|
|
1168
|
+
return new Response(error.message, {
|
|
1169
|
+
status: 403,
|
|
1170
|
+
headers: {
|
|
1171
|
+
"content-type": "text/plain; charset=utf-8"
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (error instanceof MailPreviewFormatUnavailableError) {
|
|
1176
|
+
return new Response(error.message, {
|
|
1177
|
+
status: 422,
|
|
1178
|
+
headers: {
|
|
1179
|
+
"content-type": "text/plain; charset=utf-8"
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function getMailRuntime() {
|
|
1187
|
+
return Object.freeze({
|
|
1188
|
+
sendMail,
|
|
1189
|
+
previewMail,
|
|
1190
|
+
renderMailPreview
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
var mailRuntimeInternals = {
|
|
1194
|
+
HOLO_MAIL_DELIVER_JOB,
|
|
1195
|
+
MailSendError,
|
|
1196
|
+
MailError,
|
|
1197
|
+
MailPreviewDisabledError,
|
|
1198
|
+
MailPreviewFormatUnavailableError,
|
|
1199
|
+
PendingSend,
|
|
1200
|
+
assertPreviewEnabled,
|
|
1201
|
+
builtInDrivers,
|
|
1202
|
+
createBaseSendResult,
|
|
1203
|
+
createMailRecord,
|
|
1204
|
+
createQueuedMailPayload,
|
|
1205
|
+
createResolvedMail,
|
|
1206
|
+
createSmtpAddress,
|
|
1207
|
+
createSmtpAttachment,
|
|
1208
|
+
createSmtpHeaders,
|
|
1209
|
+
createSmtpMessage,
|
|
1210
|
+
createSendContext,
|
|
1211
|
+
createResolvedMailFromQueuedPayload,
|
|
1212
|
+
computePreview,
|
|
1213
|
+
createMarkdownWrapperProps,
|
|
1214
|
+
createPreviewHtml,
|
|
1215
|
+
createPreviewPolicy,
|
|
1216
|
+
deferSendUntilCommit,
|
|
1217
|
+
deliverResolvedMail,
|
|
1218
|
+
deserializeQueuedAttachment,
|
|
1219
|
+
dispatchQueuedMail,
|
|
1220
|
+
dynamicImport,
|
|
1221
|
+
ensureMailQueueJobRegistered,
|
|
1222
|
+
escapeHtml,
|
|
1223
|
+
formatAddress,
|
|
1224
|
+
freezeSendResult,
|
|
1225
|
+
getFakeSentState,
|
|
1226
|
+
getMailerConfig,
|
|
1227
|
+
getPreviewArtifactState,
|
|
1228
|
+
getResolvedConfig,
|
|
1229
|
+
getRuntimeBindings,
|
|
1230
|
+
getRuntimeState,
|
|
1231
|
+
loadDbModule,
|
|
1232
|
+
loadNodemailerModule,
|
|
1233
|
+
loadQueueModule,
|
|
1234
|
+
loadStorageModule,
|
|
1235
|
+
isQueueRequested,
|
|
1236
|
+
normalizeExecutionDelay,
|
|
1237
|
+
normalizeExecutionString,
|
|
1238
|
+
normalizeDriverResult,
|
|
1239
|
+
persistPreviewArtifact,
|
|
1240
|
+
prepareMailSend,
|
|
1241
|
+
renderContent,
|
|
1242
|
+
renderMarkdown,
|
|
1243
|
+
renderMarkdownInline,
|
|
1244
|
+
renderPreviewResponse,
|
|
1245
|
+
renderView,
|
|
1246
|
+
resolveDriver,
|
|
1247
|
+
resolveDriverByName,
|
|
1248
|
+
resolveCurrentEnvironment,
|
|
1249
|
+
resolveEnvelope,
|
|
1250
|
+
resolveQueuePlan,
|
|
1251
|
+
resolveMailInput,
|
|
1252
|
+
resolveMailerName,
|
|
1253
|
+
resolveNodemailerModule,
|
|
1254
|
+
resolveSourceKind,
|
|
1255
|
+
resolveStorageModule,
|
|
1256
|
+
runQueuedMailDelivery,
|
|
1257
|
+
sendViaSmtp,
|
|
1258
|
+
serializeQueuedAttachment,
|
|
1259
|
+
setDbModuleLoader(loader) {
|
|
1260
|
+
getRuntimeState().loadDbModule = loader;
|
|
1261
|
+
},
|
|
1262
|
+
setNodemailerModuleLoader(loader) {
|
|
1263
|
+
getRuntimeState().loadNodemailerModule = loader;
|
|
1264
|
+
},
|
|
1265
|
+
setQueueModuleLoader(loader) {
|
|
1266
|
+
getRuntimeState().loadQueueModule = loader;
|
|
1267
|
+
},
|
|
1268
|
+
setStorageModuleLoader(loader) {
|
|
1269
|
+
getRuntimeState().loadStorageModule = loader;
|
|
1270
|
+
},
|
|
1271
|
+
stripMarkdownSyntax
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// src/index.ts
|
|
1275
|
+
import { defineMailConfig } from "@holo-js/config";
|
|
1276
|
+
var mail = Object.freeze({
|
|
1277
|
+
previewMail,
|
|
1278
|
+
renderMailPreview,
|
|
1279
|
+
sendMail
|
|
1280
|
+
});
|
|
1281
|
+
var src_default = mail;
|
|
1282
|
+
export {
|
|
1283
|
+
MailError,
|
|
1284
|
+
MailPreviewDisabledError,
|
|
1285
|
+
MailPreviewFormatUnavailableError,
|
|
1286
|
+
MailSendError,
|
|
1287
|
+
attachContent,
|
|
1288
|
+
attachFromPath,
|
|
1289
|
+
attachFromStorage,
|
|
1290
|
+
configureMailRuntime,
|
|
1291
|
+
createAttachmentMetadata,
|
|
1292
|
+
createAttachmentResolutionPlan,
|
|
1293
|
+
createAttachmentResolutionPlans,
|
|
1294
|
+
src_default as default,
|
|
1295
|
+
defineMail,
|
|
1296
|
+
defineMailConfig,
|
|
1297
|
+
getMailRuntime,
|
|
1298
|
+
getMailRuntimeBindings,
|
|
1299
|
+
getRegisteredMailDriver,
|
|
1300
|
+
inferMimeTypeFromName,
|
|
1301
|
+
isAttachmentQueueSafe,
|
|
1302
|
+
isMailDefinition,
|
|
1303
|
+
listFakeSentMails,
|
|
1304
|
+
listPreviewMailArtifacts,
|
|
1305
|
+
listRegisteredMailDrivers,
|
|
1306
|
+
mailInternals,
|
|
1307
|
+
mailRegistryInternals,
|
|
1308
|
+
mailRuntimeInternals,
|
|
1309
|
+
mergeMailDefinitionInputs,
|
|
1310
|
+
normalizeMailDefinition,
|
|
1311
|
+
previewMail,
|
|
1312
|
+
registerMailDriver,
|
|
1313
|
+
renderMailPreview,
|
|
1314
|
+
resetFakeSentMails,
|
|
1315
|
+
resetMailDriverRegistry,
|
|
1316
|
+
resetMailRuntime,
|
|
1317
|
+
resetPreviewMailArtifacts,
|
|
1318
|
+
resolveAttachmentDefinition,
|
|
1319
|
+
resolveNormalizedAttachment,
|
|
1320
|
+
sendMail
|
|
1321
|
+
};
|