@zerotal/notifications 1.7.0 → 1.7.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 +5 -5
- package/src/messages/MailMessage.ts +33 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/notifications",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zerotal/core": "1.7.
|
|
34
|
-
"@zerotal/orm": "1.7.
|
|
35
|
-
"@zerotal/queue": "1.7.
|
|
33
|
+
"@zerotal/core": "1.7.3",
|
|
34
|
+
"@zerotal/orm": "1.7.3",
|
|
35
|
+
"@zerotal/queue": "1.7.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@zerotal/broadcasting": "1.7.
|
|
38
|
+
"@zerotal/broadcasting": "1.7.3",
|
|
39
39
|
"typescript": "^5.8.0"
|
|
40
40
|
},
|
|
41
41
|
"description": "Multi-channel notifications for Zerotal — mail (SMTP/Resend), database, broadcast, Slack, and SMS.",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tryCurrentApp } from "@zerotal/core";
|
|
1
2
|
import {
|
|
2
3
|
resolveAddress,
|
|
3
4
|
type AddressInput,
|
|
@@ -6,6 +7,35 @@ import {
|
|
|
6
7
|
type MailPayload,
|
|
7
8
|
} from "../drivers/MailDriver.ts";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Make an action URL absolute.
|
|
12
|
+
*
|
|
13
|
+
* A browser resolves `/projects/4` against the page it is on. An email client
|
|
14
|
+
* has no page, so a root-relative href in a delivered message is simply dead —
|
|
15
|
+
* and this is the one place the base URL cannot be left to the caller, because
|
|
16
|
+
* the caller is usually a queue job with no request to read an origin from.
|
|
17
|
+
*
|
|
18
|
+
* `config.app.url` is the origin the app already declares for exactly this.
|
|
19
|
+
* Resolution happens at render rather than in `action()` so a message can still
|
|
20
|
+
* be constructed outside an application — a unit test, or a driver being
|
|
21
|
+
* exercised on its own — where it is returned untouched.
|
|
22
|
+
*
|
|
23
|
+
* Anything already absolute is left alone, `mailto:` and `tel:` included.
|
|
24
|
+
*/
|
|
25
|
+
function _absoluteUrl(url: string): string {
|
|
26
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//")) return url;
|
|
27
|
+
|
|
28
|
+
const base = tryCurrentApp()?.container.tryMake("config")?.get<string>("app.url");
|
|
29
|
+
if (!base) return url;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return new URL(url, base).toString();
|
|
33
|
+
} catch {
|
|
34
|
+
// A base that is not a URL is a config error, not this message's problem.
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
/** Inline text styling applied to a whole line or to a single run within a line. */
|
|
10
40
|
export interface TextStyle {
|
|
11
41
|
bold?: boolean;
|
|
@@ -302,7 +332,7 @@ export class MailMessage {
|
|
|
302
332
|
for (const l of this._introLines) parts.push(this._paragraph(l));
|
|
303
333
|
if (this._actionText && this._actionUrl) {
|
|
304
334
|
parts.push(
|
|
305
|
-
`<p style="margin:24px 0"><a href="${escapeHtml(this._actionUrl)}" ` +
|
|
335
|
+
`<p style="margin:24px 0"><a href="${escapeHtml(_absoluteUrl(this._actionUrl))}" ` +
|
|
306
336
|
`style="display:inline-block;background:#4f46e5;color:#fff;text-decoration:none;` +
|
|
307
337
|
`padding:12px 20px;border-radius:8px;font-size:14px;font-weight:600">${escapeHtml(this._actionText)}</a></p>`,
|
|
308
338
|
);
|
|
@@ -330,7 +360,8 @@ export class MailMessage {
|
|
|
330
360
|
private _renderText(): string {
|
|
331
361
|
const parts: string[] = [this._lineText(this._greeting ?? [{ text: "Hello," }])];
|
|
332
362
|
for (const l of this._introLines) parts.push(this._lineText(l));
|
|
333
|
-
if (this._actionText && this._actionUrl)
|
|
363
|
+
if (this._actionText && this._actionUrl)
|
|
364
|
+
parts.push(`${this._actionText}: ${_absoluteUrl(this._actionUrl)}`);
|
|
334
365
|
for (const l of this._outroLines) parts.push(this._lineText(l));
|
|
335
366
|
parts.push(this._lineText(this._salutation ?? [{ text: "Regards," }]));
|
|
336
367
|
return parts.join("\n\n");
|