ugly-app 0.1.1045 → 0.1.1046
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/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/server/Email.d.ts +28 -5
- package/dist/server/Email.d.ts.map +1 -1
- package/dist/server/Email.js +21 -3
- package/dist/server/Email.js.map +1 -1
- package/dist/server/EmailTemplate.d.ts +2 -0
- package/dist/server/EmailTemplate.d.ts.map +1 -1
- package/dist/server/EmailTemplate.js +2 -0
- package/dist/server/EmailTemplate.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/server/Email.sender.test.ts +115 -0
- package/src/server/Email.ts +51 -7
- package/src/server/EmailTemplate.ts +4 -0
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.1046";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
package/dist/server/Email.d.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* only works on the deployed Worker. Local Node dev surfaces a clear error.
|
|
11
11
|
*
|
|
12
12
|
* Env vars (read at send time):
|
|
13
|
-
* - CLOUDFLARE_EMAIL_SENDING_DOMAIN (the `from` is
|
|
14
|
-
* defaults to `ugly.bot` when unset
|
|
13
|
+
* - CLOUDFLARE_EMAIL_SENDING_DOMAIN (the default `from` is
|
|
14
|
+
* `noreply@<domain>`; defaults to `ugly.bot` when unset. A caller's
|
|
15
|
+
* `from` must be on this domain — the Email Service rejects any other)
|
|
15
16
|
*
|
|
16
17
|
* Docs: https://developers.cloudflare.com/email-service/api/send-emails/workers-api/
|
|
17
18
|
*/
|
|
@@ -34,7 +35,15 @@ export interface EmailOptions {
|
|
|
34
35
|
* bypass the suppression check and omit the unsubscribe footer. Default false.
|
|
35
36
|
*/
|
|
36
37
|
transactional?: boolean | undefined;
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Sender. A bare local part (`'sales'`) is placed on the sending domain; a
|
|
40
|
+
* full address must already be on it. Default `noreply`.
|
|
41
|
+
*/
|
|
42
|
+
from?: string | undefined;
|
|
43
|
+
/** Display name shown beside the sender (e.g. `'Duet'`). */
|
|
44
|
+
fromName?: string | undefined;
|
|
45
|
+
/** Where replies go. Sent as the Email Service `replyTo` field. */
|
|
46
|
+
replyTo?: string | undefined;
|
|
38
47
|
headers?: Record<string, string>;
|
|
39
48
|
}
|
|
40
49
|
/** @internal Framework-only: allows sending to a raw email address for system alerts. */
|
|
@@ -51,7 +60,15 @@ export interface InternalEmailOptions {
|
|
|
51
60
|
* bypass the suppression check and omit the unsubscribe footer. Default false.
|
|
52
61
|
*/
|
|
53
62
|
transactional?: boolean | undefined;
|
|
54
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Sender. A bare local part (`'sales'`) is placed on the sending domain; a
|
|
65
|
+
* full address must already be on it. Default `noreply`.
|
|
66
|
+
*/
|
|
67
|
+
from?: string | undefined;
|
|
68
|
+
/** Display name shown beside the sender (e.g. `'Duet'`). */
|
|
69
|
+
fromName?: string | undefined;
|
|
70
|
+
/** Where replies go. Sent as the Email Service `replyTo` field. */
|
|
71
|
+
replyTo?: string | undefined;
|
|
55
72
|
headers?: Record<string, string>;
|
|
56
73
|
}
|
|
57
74
|
/**
|
|
@@ -60,11 +77,15 @@ export interface InternalEmailOptions {
|
|
|
60
77
|
*/
|
|
61
78
|
export interface EmailBinding {
|
|
62
79
|
send(msg: {
|
|
63
|
-
from: string
|
|
80
|
+
from: string | {
|
|
81
|
+
email: string;
|
|
82
|
+
name: string;
|
|
83
|
+
};
|
|
64
84
|
to: string;
|
|
65
85
|
subject: string;
|
|
66
86
|
html?: string;
|
|
67
87
|
text?: string;
|
|
88
|
+
replyTo?: string;
|
|
68
89
|
headers?: Record<string, string>;
|
|
69
90
|
}): Promise<unknown>;
|
|
70
91
|
}
|
|
@@ -97,6 +118,8 @@ export declare function createEmailSender(): {
|
|
|
97
118
|
id?: string;
|
|
98
119
|
category?: string;
|
|
99
120
|
transactional?: boolean;
|
|
121
|
+
from?: string;
|
|
122
|
+
fromName?: string;
|
|
100
123
|
replyTo?: string;
|
|
101
124
|
}): Promise<void>;
|
|
102
125
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Email.d.ts","sourceRoot":"","sources":["../../src/server/Email.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Email.d.ts","sourceRoot":"","sources":["../../src/server/Email.ts"],"names":[],"mappings":"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"}
|
package/dist/server/Email.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* only works on the deployed Worker. Local Node dev surfaces a clear error.
|
|
11
11
|
*
|
|
12
12
|
* Env vars (read at send time):
|
|
13
|
-
* - CLOUDFLARE_EMAIL_SENDING_DOMAIN (the `from` is
|
|
14
|
-
* defaults to `ugly.bot` when unset
|
|
13
|
+
* - CLOUDFLARE_EMAIL_SENDING_DOMAIN (the default `from` is
|
|
14
|
+
* `noreply@<domain>`; defaults to `ugly.bot` when unset. A caller's
|
|
15
|
+
* `from` must be on this domain — the Email Service rejects any other)
|
|
15
16
|
*
|
|
16
17
|
* Docs: https://developers.cloudflare.com/email-service/api/send-emails/workers-api/
|
|
17
18
|
*/
|
|
@@ -31,6 +32,18 @@ export function initEmail() {
|
|
|
31
32
|
export function stopEmail() {
|
|
32
33
|
_emailBinding = null;
|
|
33
34
|
}
|
|
35
|
+
// ── Sender ──────────────────────────────────────────────────────────────────
|
|
36
|
+
function resolveFrom(from, fromName) {
|
|
37
|
+
const domain = process.env['CLOUDFLARE_EMAIL_SENDING_DOMAIN'] ?? 'ugly.bot';
|
|
38
|
+
const local = from?.trim() || 'noreply';
|
|
39
|
+
const email = local.includes('@') ? local : `${local}@${domain}`;
|
|
40
|
+
const at = email.lastIndexOf('@');
|
|
41
|
+
if (email.slice(at + 1).toLowerCase() !== domain.toLowerCase()) {
|
|
42
|
+
throw new Error(`[Email] from address ${email} is not on the sending domain ${domain}`);
|
|
43
|
+
}
|
|
44
|
+
const name = fromName?.trim();
|
|
45
|
+
return name ? { email, name } : email;
|
|
46
|
+
}
|
|
34
47
|
// ── Send ────────────────────────────────────────────────────────────────────
|
|
35
48
|
/**
|
|
36
49
|
* Send an HTML email via the Cloudflare Email Service binding.
|
|
@@ -41,6 +54,7 @@ export function stopEmail() {
|
|
|
41
54
|
*/
|
|
42
55
|
export async function emailSend(options) {
|
|
43
56
|
const to = 'to' in options ? options.to?.trim() : undefined;
|
|
57
|
+
const from = resolveFrom(options.from, options.fromName);
|
|
44
58
|
if (!to) {
|
|
45
59
|
if ('userId' in options) {
|
|
46
60
|
throw new Error("[Email] binding-only mode requires a 'to' address (userId resolution was removed with the proxy)");
|
|
@@ -79,7 +93,6 @@ export async function emailSend(options) {
|
|
|
79
93
|
if (uh)
|
|
80
94
|
headers = { ...(headers ?? {}), ...uh };
|
|
81
95
|
}
|
|
82
|
-
const from = `noreply@${process.env['CLOUDFLARE_EMAIL_SENDING_DOMAIN'] ?? 'ugly.bot'}`;
|
|
83
96
|
try {
|
|
84
97
|
await _emailBinding.send({
|
|
85
98
|
from,
|
|
@@ -87,6 +100,7 @@ export async function emailSend(options) {
|
|
|
87
100
|
subject: options.subject,
|
|
88
101
|
...(html !== undefined ? { html } : {}),
|
|
89
102
|
...(text !== undefined ? { text } : {}),
|
|
103
|
+
...(options.replyTo !== undefined ? { replyTo: options.replyTo } : {}),
|
|
90
104
|
...(headers !== undefined ? { headers } : {}),
|
|
91
105
|
});
|
|
92
106
|
// eslint-disable-next-line no-console
|
|
@@ -124,6 +138,10 @@ export function createEmailSender() {
|
|
|
124
138
|
...(options.transactional !== undefined
|
|
125
139
|
? { transactional: options.transactional }
|
|
126
140
|
: {}),
|
|
141
|
+
...(options.from !== undefined ? { from: options.from } : {}),
|
|
142
|
+
...(options.fromName !== undefined
|
|
143
|
+
? { fromName: options.fromName }
|
|
144
|
+
: {}),
|
|
127
145
|
...(options.replyTo !== undefined ? { replyTo: options.replyTo } : {}),
|
|
128
146
|
};
|
|
129
147
|
if (options.to) {
|
package/dist/server/Email.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Email.js","sourceRoot":"","sources":["../../src/server/Email.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Email.js","sourceRoot":"","sources":["../../src/server/Email.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,8 @@ export declare function sendTemplateEmail<T>(to: string, subject: string, templa
|
|
|
17
17
|
id?: string;
|
|
18
18
|
category?: string;
|
|
19
19
|
transactional?: boolean;
|
|
20
|
+
from?: string;
|
|
21
|
+
fromName?: string;
|
|
20
22
|
replyTo?: string;
|
|
21
23
|
headers?: Record<string, string>;
|
|
22
24
|
}): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailTemplate.d.ts","sourceRoot":"","sources":["../../src/server/EmailTemplate.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,YAAY,EAAE,MAAM,GACnB,gBAAgB,CAAC,CAAC,CAAC,CAMrB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC7B,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE;IACR,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,GACA,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"EmailTemplate.d.ts","sourceRoot":"","sources":["../../src/server/EmailTemplate.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,YAAY,EAAE,MAAM,GACnB,gBAAgB,CAAC,CAAC,CAAC,CAMrB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC7B,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE;IACR,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,GACA,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
|
@@ -30,6 +30,8 @@ export async function sendTemplateEmail(to, subject, template, data, options) {
|
|
|
30
30
|
...(options?.transactional !== undefined
|
|
31
31
|
? { transactional: options.transactional }
|
|
32
32
|
: {}),
|
|
33
|
+
...(options?.from !== undefined ? { from: options.from } : {}),
|
|
34
|
+
...(options?.fromName !== undefined ? { fromName: options.fromName } : {}),
|
|
33
35
|
...(options?.replyTo !== undefined ? { replyTo: options.replyTo } : {}),
|
|
34
36
|
...(options?.headers !== undefined ? { headers: options.headers } : {}),
|
|
35
37
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailTemplate.js","sourceRoot":"","sources":["../../src/server/EmailTemplate.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAMvC;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAoB;IAEpB,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAI,MAAM,CAAC,CAAC;IAC/C,OAAO;QACL,MAAM,EAAE,CAAC,IAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;KACpC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAU,EACV,OAAe,EACf,QAA6B,EAC7B,IAAO,EACP,
|
|
1
|
+
{"version":3,"file":"EmailTemplate.js","sourceRoot":"","sources":["../../src/server/EmailTemplate.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAMvC;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAoB;IAEpB,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAI,MAAM,CAAC,CAAC;IAC/C,OAAO;QACL,MAAM,EAAE,CAAC,IAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;KACpC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAU,EACV,OAAe,EACf,QAA6B,EAC7B,IAAO,EACP,OAQC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,SAAS,CAAC;QACd,EAAE;QACF,OAAO;QACP,IAAI;QACJ,GAAG,CAAC,OAAO,EAAE,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,GAAG,CAAC,OAAO,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,GAAG,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS;YACtC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE;YAC1C,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,OAAO,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,OAAO,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxE,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
3
|
"packageManager": "pnpm@10.34.5",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1046",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
7
7
|
"files": [
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.1046";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
emailSend,
|
|
4
|
+
createEmailSender,
|
|
5
|
+
setEmailBinding,
|
|
6
|
+
type EmailBinding,
|
|
7
|
+
} from './Email.js';
|
|
8
|
+
import { setSuppressionDb, type SuppressionDb } from './emailSuppression.js';
|
|
9
|
+
|
|
10
|
+
function fakeDb(): SuppressionDb {
|
|
11
|
+
const rows = new Map<string, unknown>();
|
|
12
|
+
return {
|
|
13
|
+
async getDoc(_d, id) {
|
|
14
|
+
return rows.get(id) ?? null;
|
|
15
|
+
},
|
|
16
|
+
async setDoc(_d, doc) {
|
|
17
|
+
rows.set((doc as { _id: string })._id, doc);
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
async deleteDoc(_d, id) {
|
|
21
|
+
rows.delete(id);
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Sent = Parameters<EmailBinding['send']>[0];
|
|
28
|
+
|
|
29
|
+
function bindingSpy(): EmailBinding & { calls: Sent[] } {
|
|
30
|
+
const calls: Sent[] = [];
|
|
31
|
+
return {
|
|
32
|
+
calls,
|
|
33
|
+
async send(msg) {
|
|
34
|
+
calls.push(msg);
|
|
35
|
+
return undefined;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('emailSend sender + reply-to', () => {
|
|
41
|
+
let b: ReturnType<typeof bindingSpy>;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
process.env['AUTH_SECRET'] = 'test-secret';
|
|
44
|
+
process.env['PUBLIC_APP_URL'] = 'https://duetdrive.co';
|
|
45
|
+
process.env['CLOUDFLARE_EMAIL_SENDING_DOMAIN'] = 'duetdrive.co';
|
|
46
|
+
setSuppressionDb(fakeDb());
|
|
47
|
+
b = bindingSpy();
|
|
48
|
+
setEmailBinding(b);
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
delete process.env['CLOUDFLARE_EMAIL_SENDING_DOMAIN'];
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const base = {
|
|
55
|
+
to: 'lead@example.com',
|
|
56
|
+
subject: 'Hi',
|
|
57
|
+
html: '<body>hi</body>',
|
|
58
|
+
transactional: true,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
it('defaults to noreply@<sending domain>', async () => {
|
|
62
|
+
await emailSend(base);
|
|
63
|
+
expect(b.calls[0]?.from).toBe('noreply@duetdrive.co');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('expands a bare local part onto the sending domain', async () => {
|
|
67
|
+
await emailSend({ ...base, from: 'sales' });
|
|
68
|
+
expect(b.calls[0]?.from).toBe('sales@duetdrive.co');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('accepts a full address on the sending domain, case-insensitively', async () => {
|
|
72
|
+
await emailSend({ ...base, from: 'Sales@DuetDrive.co' });
|
|
73
|
+
expect(b.calls[0]?.from).toBe('Sales@DuetDrive.co');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects a from address off the sending domain without sending', async () => {
|
|
77
|
+
await expect(
|
|
78
|
+
emailSend({ ...base, from: 'sales@elsewhere.com' }),
|
|
79
|
+
).rejects.toThrow(/sending domain/);
|
|
80
|
+
expect(b.calls.length).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('sends a display name as { email, name }', async () => {
|
|
84
|
+
await emailSend({ ...base, from: 'sales', fromName: 'Duet' });
|
|
85
|
+
expect(b.calls[0]?.from).toEqual({
|
|
86
|
+
email: 'sales@duetdrive.co',
|
|
87
|
+
name: 'Duet',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('passes replyTo through as the API field, not a header', async () => {
|
|
92
|
+
await emailSend({ ...base, replyTo: 'sales@duetdrive.co' });
|
|
93
|
+
expect(b.calls[0]?.replyTo).toBe('sales@duetdrive.co');
|
|
94
|
+
expect(b.calls[0]?.headers?.['Reply-To']).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('omits replyTo when not given', async () => {
|
|
98
|
+
await emailSend(base);
|
|
99
|
+
expect(b.calls[0]).not.toHaveProperty('replyTo');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('createEmailSender forwards from, fromName and replyTo', async () => {
|
|
103
|
+
await createEmailSender().send({
|
|
104
|
+
...base,
|
|
105
|
+
from: 'sales',
|
|
106
|
+
fromName: 'Duet',
|
|
107
|
+
replyTo: 'sales@duetdrive.co',
|
|
108
|
+
});
|
|
109
|
+
expect(b.calls[0]?.from).toEqual({
|
|
110
|
+
email: 'sales@duetdrive.co',
|
|
111
|
+
name: 'Duet',
|
|
112
|
+
});
|
|
113
|
+
expect(b.calls[0]?.replyTo).toBe('sales@duetdrive.co');
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/server/Email.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* only works on the deployed Worker. Local Node dev surfaces a clear error.
|
|
11
11
|
*
|
|
12
12
|
* Env vars (read at send time):
|
|
13
|
-
* - CLOUDFLARE_EMAIL_SENDING_DOMAIN (the `from` is
|
|
14
|
-
* defaults to `ugly.bot` when unset
|
|
13
|
+
* - CLOUDFLARE_EMAIL_SENDING_DOMAIN (the default `from` is
|
|
14
|
+
* `noreply@<domain>`; defaults to `ugly.bot` when unset. A caller's
|
|
15
|
+
* `from` must be on this domain — the Email Service rejects any other)
|
|
15
16
|
*
|
|
16
17
|
* Docs: https://developers.cloudflare.com/email-service/api/send-emails/workers-api/
|
|
17
18
|
*/
|
|
@@ -44,7 +45,15 @@ export interface EmailOptions {
|
|
|
44
45
|
* bypass the suppression check and omit the unsubscribe footer. Default false.
|
|
45
46
|
*/
|
|
46
47
|
transactional?: boolean | undefined;
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Sender. A bare local part (`'sales'`) is placed on the sending domain; a
|
|
50
|
+
* full address must already be on it. Default `noreply`.
|
|
51
|
+
*/
|
|
52
|
+
from?: string | undefined;
|
|
53
|
+
/** Display name shown beside the sender (e.g. `'Duet'`). */
|
|
54
|
+
fromName?: string | undefined;
|
|
55
|
+
/** Where replies go. Sent as the Email Service `replyTo` field. */
|
|
56
|
+
replyTo?: string | undefined;
|
|
48
57
|
headers?: Record<string, string>;
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -62,7 +71,15 @@ export interface InternalEmailOptions {
|
|
|
62
71
|
* bypass the suppression check and omit the unsubscribe footer. Default false.
|
|
63
72
|
*/
|
|
64
73
|
transactional?: boolean | undefined;
|
|
65
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Sender. A bare local part (`'sales'`) is placed on the sending domain; a
|
|
76
|
+
* full address must already be on it. Default `noreply`.
|
|
77
|
+
*/
|
|
78
|
+
from?: string | undefined;
|
|
79
|
+
/** Display name shown beside the sender (e.g. `'Duet'`). */
|
|
80
|
+
fromName?: string | undefined;
|
|
81
|
+
/** Where replies go. Sent as the Email Service `replyTo` field. */
|
|
82
|
+
replyTo?: string | undefined;
|
|
66
83
|
headers?: Record<string, string>;
|
|
67
84
|
}
|
|
68
85
|
|
|
@@ -74,11 +91,13 @@ export interface InternalEmailOptions {
|
|
|
74
91
|
*/
|
|
75
92
|
export interface EmailBinding {
|
|
76
93
|
send(msg: {
|
|
77
|
-
from: string;
|
|
94
|
+
from: string | { email: string; name: string };
|
|
78
95
|
to: string;
|
|
79
96
|
subject: string;
|
|
80
97
|
html?: string;
|
|
81
98
|
text?: string;
|
|
99
|
+
// Reply-To is rejected as a custom header (E_HEADER_USE_API_FIELD).
|
|
100
|
+
replyTo?: string;
|
|
82
101
|
// Custom headers (allowlisted by the Email Service — e.g. List-Unsubscribe,
|
|
83
102
|
// List-Unsubscribe-Post, X-*). See /email-service/reference/headers/.
|
|
84
103
|
headers?: Record<string, string>;
|
|
@@ -105,6 +124,25 @@ export function stopEmail(): void {
|
|
|
105
124
|
_emailBinding = null;
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
// ── Sender ──────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function resolveFrom(
|
|
130
|
+
from: string | undefined,
|
|
131
|
+
fromName: string | undefined,
|
|
132
|
+
): string | { email: string; name: string } {
|
|
133
|
+
const domain = process.env['CLOUDFLARE_EMAIL_SENDING_DOMAIN'] ?? 'ugly.bot';
|
|
134
|
+
const local = from?.trim() || 'noreply';
|
|
135
|
+
const email = local.includes('@') ? local : `${local}@${domain}`;
|
|
136
|
+
const at = email.lastIndexOf('@');
|
|
137
|
+
if (email.slice(at + 1).toLowerCase() !== domain.toLowerCase()) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`[Email] from address ${email} is not on the sending domain ${domain}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const name = fromName?.trim();
|
|
143
|
+
return name ? { email, name } : email;
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
// ── Send ────────────────────────────────────────────────────────────────────
|
|
109
147
|
|
|
110
148
|
/**
|
|
@@ -118,6 +156,7 @@ export async function emailSend(
|
|
|
118
156
|
options: EmailOptions | InternalEmailOptions,
|
|
119
157
|
): Promise<void> {
|
|
120
158
|
const to = 'to' in options ? options.to?.trim() : undefined;
|
|
159
|
+
const from = resolveFrom(options.from, options.fromName);
|
|
121
160
|
if (!to) {
|
|
122
161
|
if ('userId' in options) {
|
|
123
162
|
throw new Error(
|
|
@@ -165,8 +204,6 @@ export async function emailSend(
|
|
|
165
204
|
if (uh) headers = { ...(headers ?? {}), ...uh };
|
|
166
205
|
}
|
|
167
206
|
|
|
168
|
-
const from = `noreply@${process.env['CLOUDFLARE_EMAIL_SENDING_DOMAIN'] ?? 'ugly.bot'}`;
|
|
169
|
-
|
|
170
207
|
try {
|
|
171
208
|
await _emailBinding.send({
|
|
172
209
|
from,
|
|
@@ -174,6 +211,7 @@ export async function emailSend(
|
|
|
174
211
|
subject: options.subject,
|
|
175
212
|
...(html !== undefined ? { html } : {}),
|
|
176
213
|
...(text !== undefined ? { text } : {}),
|
|
214
|
+
...(options.replyTo !== undefined ? { replyTo: options.replyTo } : {}),
|
|
177
215
|
...(headers !== undefined ? { headers } : {}),
|
|
178
216
|
});
|
|
179
217
|
// eslint-disable-next-line no-console
|
|
@@ -210,6 +248,8 @@ export function createEmailSender() {
|
|
|
210
248
|
id?: string;
|
|
211
249
|
category?: string;
|
|
212
250
|
transactional?: boolean;
|
|
251
|
+
from?: string;
|
|
252
|
+
fromName?: string;
|
|
213
253
|
replyTo?: string;
|
|
214
254
|
}): Promise<void> {
|
|
215
255
|
const extra = {
|
|
@@ -221,6 +261,10 @@ export function createEmailSender() {
|
|
|
221
261
|
...(options.transactional !== undefined
|
|
222
262
|
? { transactional: options.transactional }
|
|
223
263
|
: {}),
|
|
264
|
+
...(options.from !== undefined ? { from: options.from } : {}),
|
|
265
|
+
...(options.fromName !== undefined
|
|
266
|
+
? { fromName: options.fromName }
|
|
267
|
+
: {}),
|
|
224
268
|
...(options.replyTo !== undefined ? { replyTo: options.replyTo } : {}),
|
|
225
269
|
};
|
|
226
270
|
if (options.to) {
|
|
@@ -36,6 +36,8 @@ export async function sendTemplateEmail<T>(
|
|
|
36
36
|
id?: string;
|
|
37
37
|
category?: string;
|
|
38
38
|
transactional?: boolean;
|
|
39
|
+
from?: string;
|
|
40
|
+
fromName?: string;
|
|
39
41
|
replyTo?: string;
|
|
40
42
|
headers?: Record<string, string>;
|
|
41
43
|
},
|
|
@@ -50,6 +52,8 @@ export async function sendTemplateEmail<T>(
|
|
|
50
52
|
...(options?.transactional !== undefined
|
|
51
53
|
? { transactional: options.transactional }
|
|
52
54
|
: {}),
|
|
55
|
+
...(options?.from !== undefined ? { from: options.from } : {}),
|
|
56
|
+
...(options?.fromName !== undefined ? { fromName: options.fromName } : {}),
|
|
53
57
|
...(options?.replyTo !== undefined ? { replyTo: options.replyTo } : {}),
|
|
54
58
|
...(options?.headers !== undefined ? { headers: options.headers } : {}),
|
|
55
59
|
});
|