@10x-media/form-builder 0.1.0-beta.16 → 0.1.0-beta.18
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -21
- package/dist/actions/builtin/emailAction.d.ts +4 -3
- package/dist/actions/builtin/emailAction.js +11 -5
- package/dist/actions/builtin/emailAction.js.map +1 -1
- package/dist/actions/fromAddresses.d.ts +17 -1
- package/dist/actions/fromAddresses.js +62 -6
- package/dist/actions/fromAddresses.js.map +1 -1
- package/dist/actions/recipientSources.js +1 -1
- package/dist/actions/recipientSources.js.map +1 -1
- package/dist/collections/forms.js +2 -1
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/formsEndpoints.js +4 -3
- package/dist/collections/formsEndpoints.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +12 -2
- package/dist/plugin/registerCollections.js +2 -1
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/react/Poll.d.ts +10 -0
- package/dist/react/Poll.js +5 -4
- package/dist/react/Poll.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @10x-media/form-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.18
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `email.fromSources`: senders resolved at send time, for hosts where the from-address is tenant identity rather than per-form configuration. A source stores a stable namespaced value (e.g. `tenant:default`) on the action and re-resolves the actual address on every send with the same run-time arguments a recipient source gets, so a tenant that changes its from-address changes it for every saved form at once. Sources appear in the existing From select ahead of the `fromAddresses` literals; literals keep today's behaviour exactly, including never touching a source at send.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-beta.17
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Add `renderResults` to `<Poll>`: a render prop that replaces the built-in `<FormResults>` wherever the poll shows results (after voting, when closed, and for a recorded outcome). It receives the exact `FormResultsProps` the default rendering gets, so hosts can draw result rows from their own data, keyed by each bucket's option value, or wrap `<FormResults>` in their own layout. Omitting the prop changes nothing.
|
|
14
|
+
|
|
3
15
|
## 0.1.0-beta.16
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 10x Media GmbH
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10x Media GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FromAddressesResolver } from "../fromAddresses.js";
|
|
2
|
-
import { DepartmentEmailsResolver } from "../../email/departments.js";
|
|
3
1
|
import { RecipientSourceRegistry } from "../recipientSources.js";
|
|
2
|
+
import { FromAddressSourceRegistry, FromAddressesResolver } from "../fromAddresses.js";
|
|
3
|
+
import { DepartmentEmailsResolver } from "../../email/departments.js";
|
|
4
4
|
import { RecipientsConfig } from "../emailRecipients.js";
|
|
5
5
|
import { Field, RichTextField } from "payload";
|
|
6
6
|
|
|
@@ -9,7 +9,8 @@ import { Field, RichTextField } from "payload";
|
|
|
9
9
|
type EmailActionOptions = {
|
|
10
10
|
localize: boolean;
|
|
11
11
|
editor?: RichTextField['editor'];
|
|
12
|
-
fromAddresses?: FromAddressesResolver;
|
|
12
|
+
fromAddresses?: FromAddressesResolver; /** Send-time-resolved senders offered in the from select (plugin option `email.fromSources`). */
|
|
13
|
+
fromSources?: FromAddressSourceRegistry;
|
|
13
14
|
departments?: DepartmentEmailsResolver;
|
|
14
15
|
recipients?: RecipientsConfig; /** Server-resolved recipient sources offered in every recipient list (plugin option `email.recipientSources`). */
|
|
15
16
|
recipientSources?: RecipientSourceRegistry;
|
|
@@ -5,7 +5,7 @@ import { interpolate } from "../../recall/interpolate.js";
|
|
|
5
5
|
import { buildRecipientField, resolveRecipientEntries } from "../emailRecipients.js";
|
|
6
6
|
import { resolverFor } from "../body/serializeBody.js";
|
|
7
7
|
import { defineAction } from "../defineAction.js";
|
|
8
|
-
import { buildFromField } from "../fromAddresses.js";
|
|
8
|
+
import { buildFromField, resolveSendFrom } from "../fromAddresses.js";
|
|
9
9
|
import { sourcesByValue } from "../recipientSources.js";
|
|
10
10
|
//#region src/actions/builtin/emailAction.ts
|
|
11
11
|
/**
|
|
@@ -17,7 +17,8 @@ import { sourcesByValue } from "../recipientSources.js";
|
|
|
17
17
|
* primary `to` target and its missing-value behavior differ, threaded through `spec`.
|
|
18
18
|
*/
|
|
19
19
|
const buildEmailAction = (options, spec) => {
|
|
20
|
-
const { localize, editor, fromAddresses, departments, recipients, recipientSources } = options;
|
|
20
|
+
const { localize, editor, fromAddresses, fromSources, departments, recipients, recipientSources } = options;
|
|
21
|
+
const fromSourcesByValue = sourcesByValue(fromSources);
|
|
21
22
|
const endpoint = departments ? "departments" : void 0;
|
|
22
23
|
const recip = (name, labelKey) => buildRecipientField(name, labelKey, localize, {
|
|
23
24
|
endpoint,
|
|
@@ -34,7 +35,7 @@ const buildEmailAction = (options, spec) => {
|
|
|
34
35
|
type: "row",
|
|
35
36
|
fields: [spec.target(recip), recip("replyTo", keys.actionConfigReplyTo)]
|
|
36
37
|
},
|
|
37
|
-
...fromAddresses ? [buildFromField(fromAddresses)] : [],
|
|
38
|
+
...fromAddresses || fromSources ? [buildFromField(fromAddresses, fromSources)] : [],
|
|
38
39
|
{
|
|
39
40
|
type: "row",
|
|
40
41
|
fields: [recip("cc", keys.actionConfigCc), recip("bcc", keys.actionConfigBcc)]
|
|
@@ -57,7 +58,7 @@ const buildEmailAction = (options, spec) => {
|
|
|
57
58
|
run: async (args) => {
|
|
58
59
|
const { config, values } = args;
|
|
59
60
|
const resolve = resolverFor(values);
|
|
60
|
-
const sources = sourcesByValue(
|
|
61
|
+
const sources = sourcesByValue(recipientSources);
|
|
61
62
|
const sourceArgs = {
|
|
62
63
|
context: args.context,
|
|
63
64
|
values,
|
|
@@ -96,11 +97,16 @@ const buildEmailAction = (options, spec) => {
|
|
|
96
97
|
sources,
|
|
97
98
|
sourceArgs
|
|
98
99
|
})).join(", ");
|
|
100
|
+
const from = await resolveSendFrom({
|
|
101
|
+
configured: config.from,
|
|
102
|
+
sources: fromSourcesByValue,
|
|
103
|
+
sourceArgs
|
|
104
|
+
});
|
|
99
105
|
await args.payload.sendEmail({
|
|
100
106
|
to,
|
|
101
107
|
subject,
|
|
102
108
|
html,
|
|
103
|
-
...
|
|
109
|
+
...from ? { from } : {},
|
|
104
110
|
...cc ? { cc } : {},
|
|
105
111
|
...bcc ? { bcc } : {},
|
|
106
112
|
...replyTo ? { replyTo } : {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emailAction.js","names":[],"sources":["../../../src/actions/builtin/emailAction.ts"],"sourcesContent":["import type { Field, RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { type ActionDefinition, defineAction } from '../defineAction'\nimport {\n\tbuildRecipientField,\n\ttype RecipientsConfig,\n\tresolveRecipientEntries,\n} from '../emailRecipients'\nimport {
|
|
1
|
+
{"version":3,"file":"emailAction.js","names":[],"sources":["../../../src/actions/builtin/emailAction.ts"],"sourcesContent":["import type { Field, RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { type ActionDefinition, defineAction } from '../defineAction'\nimport {\n\tbuildRecipientField,\n\ttype RecipientsConfig,\n\tresolveRecipientEntries,\n} from '../emailRecipients'\nimport {\n\tbuildFromField,\n\ttype FromAddressesResolver,\n\ttype FromAddressSourceRegistry,\n\tresolveSendFrom,\n} from '../fromAddresses'\nimport {\n\ttype RecipientResolveArgs,\n\ttype RecipientSource,\n\ttype RecipientSourceRegistry,\n\tsourcesByValue,\n} from '../recipientSources'\n\n/** The plugin-derived options every built-in email action is built from (was five positional args). */\nexport type EmailActionOptions = {\n\tlocalize: boolean\n\teditor?: RichTextField['editor']\n\tfromAddresses?: FromAddressesResolver\n\t/** Send-time-resolved senders offered in the from select (plugin option `email.fromSources`). */\n\tfromSources?: FromAddressSourceRegistry\n\tdepartments?: DepartmentEmailsResolver\n\trecipients?: RecipientsConfig\n\t/** Server-resolved recipient sources offered in every recipient list (plugin option `email.recipientSources`). */\n\trecipientSources?: RecipientSourceRegistry\n}\n\n/** The config fields shared by every built-in email action (each action adds its own `to` target). */\nexport type EmailActionConfig = {\n\tfrom?: string\n\tcc?: string[]\n\tbcc?: string[]\n\treplyTo?: string[]\n\tsubject?: string\n\tbody?: unknown\n}\n\n/** Builds a recipient-list field (`to`/`cc`/`bcc`/`replyTo`) with the shared width, endpoint, and options. */\ntype RecipientFieldBuilder = (name: string, labelKey: string) => Field\n\ntype Resolver = ReturnType<typeof resolverFor>\n\n/** What `resolveTo` needs to compute the primary target, including server-resolved sources. */\ntype ResolveToArgs<TConfig extends EmailActionConfig> = {\n\tconfig: TConfig\n\tresolve: Resolver\n\tsources: Map<string, RecipientSource>\n\tsourceArgs: RecipientResolveArgs\n}\n\n/** What distinguishes one email action from another: identity, its primary target, and how it resolves/guards that target. */\ntype EmailActionSpec<TConfig extends EmailActionConfig> = {\n\ttype: string\n\tlabel: string\n\t/** The first cell of the opening row (paired with `replyTo`): a recipient list, or an email-field select. */\n\ttarget: (recip: RecipientFieldBuilder) => Field\n\t/** Resolve the primary `to` to a comma-joined address string, or `''` when nothing resolves. */\n\tresolveTo: (args: ResolveToArgs<TConfig>) => Promise<string> | string\n\t/**\n\t * Whether the author configured any target at all. A configured target that resolves empty (e.g. a\n\t * source returned `[]`) is a normal skip; only a target the author never set is a misconfiguration.\n\t */\n\thasTarget: (config: TConfig) => boolean\n\t/** With no target authored, `emailTeam` treats it as a misconfiguration (`throw`), `confirmation` as a silent skip. */\n\tonMissingTo: 'throw' | 'skip'\n}\n\n/**\n * The shared skeleton of the built-in email actions (`emailTeam`, `confirmation`): an identical\n * config (a first row pairing the action's target with `replyTo`, an optional `from` select, a\n * cc/bcc row, a subject, and a rich text body, content and recipient fields carrying `localized`\n * when `localize`) and an identical send (interpolate the subject, render the body, resolve\n * cc/bcc/replyTo, and hand a single comma-joined string per list to `payload.sendEmail`). Only the\n * primary `to` target and its missing-value behavior differ, threaded through `spec`.\n */\nexport const buildEmailAction = <TConfig extends EmailActionConfig>(\n\toptions: EmailActionOptions,\n\tspec: EmailActionSpec<TConfig>\n): ActionDefinition<TConfig> => {\n\tconst {\n\t\tlocalize,\n\t\teditor,\n\t\tfromAddresses,\n\t\tfromSources,\n\t\tdepartments,\n\t\trecipients,\n\t\trecipientSources,\n\t} = options\n\tconst fromSourcesByValue = sourcesByValue(fromSources)\n\tconst endpoint = departments ? 'departments' : undefined\n\tconst recip: RecipientFieldBuilder = (name, labelKey) =>\n\t\tbuildRecipientField(name, labelKey, localize, {\n\t\t\tendpoint,\n\t\t\trecipients,\n\t\t\twidth: '50%',\n\t\t\tdepartments,\n\t\t\tsources: recipientSources,\n\t\t})\n\treturn defineAction<TConfig>({\n\t\ttype: spec.type,\n\t\tlabel: spec.label,\n\t\tconfig: [\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [spec.target(recip), recip('replyTo', keys.actionConfigReplyTo)],\n\t\t\t},\n\t\t\t...(fromAddresses || fromSources ? [buildFromField(fromAddresses, fromSources)] : []),\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('cc', keys.actionConfigCc), recip('bcc', keys.actionConfigBcc)],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values } = args\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst sources = sourcesByValue(recipientSources)\n\t\t\tconst sourceArgs: RecipientResolveArgs = {\n\t\t\t\tcontext: args.context,\n\t\t\t\tvalues,\n\t\t\t\tdescriptors: args.descriptors,\n\t\t\t\tform: args.form,\n\t\t\t\tsubmissionId: args.submissionId,\n\t\t\t\tpayload: args.payload,\n\t\t\t\treq: args.req,\n\t\t\t\tlocale: args.locale,\n\t\t\t}\n\t\t\tconst to = await spec.resolveTo({ config, resolve, sources, sourceArgs })\n\t\t\tif (!to) {\n\t\t\t\t// Nothing to send to. A target the author never configured is a misconfiguration (emailTeam\n\t\t\t\t// throws); a configured target that resolved empty (e.g. a source returned []) is a normal skip.\n\t\t\t\tif (!spec.hasTarget(config) && spec.onMissingTo === 'throw') {\n\t\t\t\t\tthrow new Error(`${spec.type}: missing \"to\" address`)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof args.payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error(`${spec.type}: no email adapter configured`)\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await args.renderBody(config.body)\n\t\t\tconst cc = (await resolveRecipientEntries(config.cc, { resolve, sources, sourceArgs })).join(\n\t\t\t\t', '\n\t\t\t)\n\t\t\tconst bcc = (\n\t\t\t\tawait resolveRecipientEntries(config.bcc, { resolve, sources, sourceArgs })\n\t\t\t).join(', ')\n\t\t\tconst replyTo = (\n\t\t\t\tawait resolveRecipientEntries(config.replyTo, { resolve, sources, sourceArgs })\n\t\t\t).join(', ')\n\n\t\t\t// A literal `from` was validated at save time against `fromAddresses(req)`; not re-checked\n\t\t\t// here (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so it is forwarded verbatim. A stored source\n\t\t\t// value instead resolves freshly on every send, so the sender follows the host (e.g. a\n\t\t\t// tenant that changed its address) rather than freezing at authoring time.\n\t\t\tconst from = await resolveSendFrom({\n\t\t\t\tconfigured: config.from,\n\t\t\t\tsources: fromSourcesByValue,\n\t\t\t\tsourceArgs,\n\t\t\t})\n\t\t\tawait args.payload.sendEmail({\n\t\t\t\tto,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(from ? { from } : {}),\n\t\t\t\t...(cc ? { cc } : {}),\n\t\t\t\t...(bcc ? { bcc } : {}),\n\t\t\t\t...(replyTo ? { replyTo } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuFA,MAAa,oBACZ,SACA,SAC+B;CAC/B,MAAM,EACL,UACA,QACA,eACA,aACA,aACA,YACA,qBACG;CACJ,MAAM,qBAAqB,eAAe,WAAW;CACrD,MAAM,WAAW,cAAc,gBAAgB,KAAA;CAC/C,MAAM,SAAgC,MAAM,aAC3C,oBAAoB,MAAM,UAAU,UAAU;EAC7C;EACA;EACA,OAAO;EACP;EACA,SAAS;CACV,CAAC;CACF,OAAO,aAAsB;EAC5B,MAAM,KAAK;EACX,OAAO,KAAK;EACZ,QAAQ;GACP;IACC,MAAM;IACN,QAAQ,CAAC,KAAK,OAAO,KAAK,GAAG,MAAM,WAAW,KAAK,mBAAmB,CAAC;GACxE;GACA,GAAI,iBAAiB,cAAc,CAAC,eAAe,eAAe,WAAW,CAAC,IAAI,CAAC;GACnF;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,OAAO,KAAK,eAAe,CAAC;GAC9E;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,WAAW;GAC3B,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,UAAU,eAAe,gBAAgB;GAC/C,MAAM,aAAmC;IACxC,SAAS,KAAK;IACd;IACA,aAAa,KAAK;IAClB,MAAM,KAAK;IACX,cAAc,KAAK;IACnB,SAAS,KAAK;IACd,KAAK,KAAK;IACV,QAAQ,KAAK;GACd;GACA,MAAM,KAAK,MAAM,KAAK,UAAU;IAAE;IAAQ;IAAS;IAAS;GAAW,CAAC;GACxE,IAAI,CAAC,IAAI;IAGR,IAAI,CAAC,KAAK,UAAU,MAAM,KAAK,KAAK,gBAAgB,SACnD,MAAM,IAAI,MAAM,GAAG,KAAK,KAAK,uBAAuB;IAErD;GACD;GACA,IAAI,OAAO,KAAK,QAAQ,cAAc,YACrC,MAAM,IAAI,MAAM,GAAG,KAAK,KAAK,8BAA8B;GAG5D,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,KAAK,WAAW,OAAO,IAAI;GAC9C,MAAM,MAAM,MAAM,wBAAwB,OAAO,IAAI;IAAE;IAAS;IAAS;GAAW,CAAC,GAAG,KACvF,IACD;GACA,MAAM,OACL,MAAM,wBAAwB,OAAO,KAAK;IAAE;IAAS;IAAS;GAAW,CAAC,GACzE,KAAK,IAAI;GACX,MAAM,WACL,MAAM,wBAAwB,OAAO,SAAS;IAAE;IAAS;IAAS;GAAW,CAAC,GAC7E,KAAK,IAAI;GAOX,MAAM,OAAO,MAAM,gBAAgB;IAClC,YAAY,OAAO;IACnB,SAAS;IACT;GACD,CAAC;GACD,MAAM,KAAK,QAAQ,UAAU;IAC5B;IACA;IACA;IACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;IACvB,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;IACnB,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IACrB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;GAC9B,CAAC;EACF;CACD,CAAC;AACF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RecipientResolveArgs } from "./recipientSources.js";
|
|
1
2
|
import { PayloadRequest } from "payload";
|
|
2
3
|
|
|
3
4
|
//#region src/actions/fromAddresses.d.ts
|
|
@@ -6,6 +7,21 @@ type FromAddressOption = {
|
|
|
6
7
|
label: string;
|
|
7
8
|
value: string;
|
|
8
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* A sender the plugin resolves server-side at send time (plugin option `email.fromSources`), the
|
|
12
|
+
* from-side counterpart of a `RecipientSource`. `value` is the namespaced string stored on the
|
|
13
|
+
* action (e.g. `tenant:default`), so it cannot collide with a literal address and stays
|
|
14
|
+
* audit-stable while the address it resolves to follows the host; `label` is what the editor sees
|
|
15
|
+
* in the from select. `resolve` returns the address to send from right now (reduced to a single
|
|
16
|
+
* address), or null/empty to send with the email adapter's default sender. A throw fails the
|
|
17
|
+
* action loudly (and retries on the queued path) rather than sending as the wrong identity.
|
|
18
|
+
*/
|
|
19
|
+
type FromAddressSource = {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string | Record<string, string>;
|
|
22
|
+
resolve: (args: RecipientResolveArgs) => Promise<string | null> | string | null;
|
|
23
|
+
};
|
|
24
|
+
type FromAddressSourceRegistry = Record<string, FromAddressSource>;
|
|
9
25
|
/**
|
|
10
26
|
* Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`
|
|
11
27
|
* (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`
|
|
@@ -17,5 +33,5 @@ type FromAddressesResolver = (args: {
|
|
|
17
33
|
req: PayloadRequest;
|
|
18
34
|
}) => Promise<FromAddressOption[]> | FromAddressOption[];
|
|
19
35
|
//#endregion
|
|
20
|
-
export { FromAddressOption, FromAddressesResolver };
|
|
36
|
+
export { FromAddressOption, FromAddressSource, FromAddressSourceRegistry, FromAddressesResolver };
|
|
21
37
|
//# sourceMappingURL=fromAddresses.d.ts.map
|
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
import { keys } from "../translations/keys.js";
|
|
2
2
|
import { asTranslate, labelFor } from "../translations/server.js";
|
|
3
|
+
import { isPlausibleEmail } from "./emailRecipients.js";
|
|
3
4
|
//#region src/actions/fromAddresses.ts
|
|
5
|
+
/**
|
|
6
|
+
* The `from` handed to `payload.sendEmail`: a stored source value re-resolves through its source
|
|
7
|
+
* with the run-time args; anything else (a literal picked from `fromAddresses`, which never
|
|
8
|
+
* touches a source at send) is forwarded verbatim, and no configured value means no `from` at
|
|
9
|
+
* all. Mirrors `resolveRecipientEntries`: a throwing source propagates.
|
|
10
|
+
*/
|
|
11
|
+
const resolveSendFrom = async (opts) => {
|
|
12
|
+
const { configured, sources, sourceArgs } = opts;
|
|
13
|
+
if (!configured) return;
|
|
14
|
+
const source = sources?.get(configured);
|
|
15
|
+
if (!source) return configured;
|
|
16
|
+
const resolved = sourceArgs ? await source.resolve(sourceArgs) : null;
|
|
17
|
+
if (!resolved) return;
|
|
18
|
+
return firstSender(resolved) || void 0;
|
|
19
|
+
};
|
|
20
|
+
/** A bare plausible address, or a `Name <addr>` display form wrapping one (quotes and commas in the name included). */
|
|
21
|
+
const isPlausibleSender = (value) => {
|
|
22
|
+
if (isPlausibleEmail(value)) return true;
|
|
23
|
+
const bracketed = /^[^<>]*<([^<>\s]+)>$/.exec(value);
|
|
24
|
+
return Boolean(bracketed?.[1] && isPlausibleEmail(bracketed[1]));
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The sender-side counterpart of `firstAddress`: one sender only, but `Name <addr>` display form
|
|
28
|
+
* survives because that is the documented shape of a `from`. Order matters: cut at line breaks
|
|
29
|
+
* first (the header-injection vector), accept the whole remaining line so a quoted display name
|
|
30
|
+
* may contain commas, and only then comma-split to clamp a multi-address result to its first
|
|
31
|
+
* entry. An implausible result becomes empty (send with the adapter default) rather than a
|
|
32
|
+
* broken header.
|
|
33
|
+
*/
|
|
34
|
+
const firstSender = (value) => {
|
|
35
|
+
const line = (value.split(/[\n\r]+/)[0] ?? "").trim();
|
|
36
|
+
if (isPlausibleSender(line)) return line;
|
|
37
|
+
const [first] = line.split(/[,;]+/);
|
|
38
|
+
const cleaned = (first ?? "").trim();
|
|
39
|
+
return isPlausibleSender(cleaned) ? cleaned : "";
|
|
40
|
+
};
|
|
4
41
|
const FROM_FIELD_REF = "@10x-media/form-builder/client#EndpointOptionsSelect";
|
|
5
42
|
/**
|
|
6
43
|
* Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's
|
|
@@ -15,8 +52,10 @@ const FROM_FIELD_REF = "@10x-media/form-builder/client#EndpointOptionsSelect";
|
|
|
15
52
|
* unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.
|
|
16
53
|
* A resolver reaching a flaky upstream should cache or fall back internally rather than throw.
|
|
17
54
|
*/
|
|
18
|
-
const validateFromField = (resolver) => async (value, { req }) => {
|
|
55
|
+
const validateFromField = (resolver, sourceValues) => async (value, { req }) => {
|
|
19
56
|
if (typeof value !== "string" || value.length === 0) return true;
|
|
57
|
+
if (sourceValues?.has(value)) return true;
|
|
58
|
+
if (!resolver) return asTranslate(req.t)(keys.validationFromUnknown);
|
|
20
59
|
let options;
|
|
21
60
|
try {
|
|
22
61
|
options = await resolver({ req });
|
|
@@ -31,11 +70,11 @@ const validateFromField = (resolver) => async (value, { req }) => {
|
|
|
31
70
|
* is set). That route's document id goes unused server-side: the option set is request-scoped, not
|
|
32
71
|
* per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.
|
|
33
72
|
*/
|
|
34
|
-
const buildFromField = (resolver) => ({
|
|
73
|
+
const buildFromField = (resolver, sources) => ({
|
|
35
74
|
name: "from",
|
|
36
75
|
type: "text",
|
|
37
76
|
label: labelFor(keys.actionConfigFrom),
|
|
38
|
-
validate: validateFromField(resolver),
|
|
77
|
+
validate: validateFromField(resolver, sources ? new Set(Object.values(sources).map((source) => source.value)) : void 0),
|
|
39
78
|
admin: { components: { Field: {
|
|
40
79
|
path: FROM_FIELD_REF,
|
|
41
80
|
clientProps: {
|
|
@@ -45,21 +84,38 @@ const buildFromField = (resolver) => ({
|
|
|
45
84
|
} } }
|
|
46
85
|
});
|
|
47
86
|
/**
|
|
87
|
+
* A source entry as the from select shows it. A string label is display text served raw (matching
|
|
88
|
+
* how `RecipientsSelect` receives source labels); a per-locale record picks the request's admin
|
|
89
|
+
* language, then English, then any value.
|
|
90
|
+
*/
|
|
91
|
+
const sourceOption = (source, req) => {
|
|
92
|
+
if (typeof source.label === "string") return {
|
|
93
|
+
label: source.label,
|
|
94
|
+
value: source.value
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
label: source.label[req.i18n.language] ?? source.label.en ?? Object.values(source.label)[0] ?? source.value,
|
|
98
|
+
value: source.value
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
48
102
|
* Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:
|
|
49
103
|
* authenticated callers get the host resolver's current options for this request; anonymous
|
|
50
104
|
* callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror
|
|
51
105
|
* the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).
|
|
52
106
|
*/
|
|
53
107
|
const resolveFromAddressesRequest = async (args) => {
|
|
54
|
-
const { isAuthed, req, resolver } = args;
|
|
108
|
+
const { isAuthed, req, resolver, sources } = args;
|
|
55
109
|
if (!isAuthed) return {
|
|
56
110
|
status: 403,
|
|
57
111
|
body: { errors: [{ message: "Forbidden" }] }
|
|
58
112
|
};
|
|
59
113
|
try {
|
|
114
|
+
const sourceOptions = Object.values(sources ?? {}).map((source) => sourceOption(source, req));
|
|
115
|
+
const resolved = resolver ? await resolver({ req }) : [];
|
|
60
116
|
return {
|
|
61
117
|
status: 200,
|
|
62
|
-
body: { options:
|
|
118
|
+
body: { options: [...sourceOptions, ...resolved] }
|
|
63
119
|
};
|
|
64
120
|
} catch {
|
|
65
121
|
return {
|
|
@@ -69,6 +125,6 @@ const resolveFromAddressesRequest = async (args) => {
|
|
|
69
125
|
}
|
|
70
126
|
};
|
|
71
127
|
//#endregion
|
|
72
|
-
export { buildFromField, resolveFromAddressesRequest };
|
|
128
|
+
export { buildFromField, resolveFromAddressesRequest, resolveSendFrom };
|
|
73
129
|
|
|
74
130
|
//# sourceMappingURL=fromAddresses.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fromAddresses.js","names":[],"sources":["../../src/actions/fromAddresses.ts"],"sourcesContent":["import type { PayloadRequest, TextField } from 'payload'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\n\n/** One selectable \"from\" address for the built-in email actions. */\nexport type FromAddressOption = { label: string; value: string }\n\n/**\n * Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`\n * (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`\n * (host header, cookie, or auth context) and return only that tenant's allowed senders. `value`\n * is the literal string handed to `payload.sendEmail`'s `from` (e.g. `'Name <addr@x.com>'` or a\n * plain address). Absent keeps the email adapter's default sender and adds no `from` field at all.\n */\nexport type FromAddressesResolver = (args: {\n\treq: PayloadRequest\n}) => Promise<FromAddressOption[]> | FromAddressOption[]\n\nconst FROM_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's\n * `toField` and poll's `resultsField`): unset is fine, otherwise the value must be one of the\n * resolver's options for this request. A throwing resolver fails closed with a translated message\n * rather than surfacing a raw error on save.\n *\n * Failing closed has an operational cost worth knowing: Payload runs this on every save, not only\n * when `from` changed, so for as long as the resolver is down no form carrying an email action with\n * a `from` set can be saved at all, including edits that never touch the address. That is the\n * deliberate trade: failing open would persist a sender the host can no longer vouch for, and\n * unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.\n * A resolver reaching a flaky upstream should cache or fall back internally rather than throw.\n */\nexport const validateFromField =\n\t(resolver: FromAddressesResolver) =>\n\tasync (value: unknown, { req }: { req: PayloadRequest }): Promise<string | true> => {\n\t\tif (typeof value !== 'string' || value.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\tlet options: FromAddressOption[]\n\t\ttry {\n\t\t\toptions = await resolver({ req })\n\t\t} catch {\n\t\t\treturn asTranslate(req.t)(keys.validationFromUnavailable)\n\t\t}\n\t\treturn options.some((option) => option.value === value)\n\t\t\t? true\n\t\t\t: asTranslate(req.t)(keys.validationFromUnknown)\n\t}\n\n/**\n * The `from` select shared by `emailTeam` and `confirmation`: an `EndpointOptionsSelect` backed by\n * the forms collection's `/:id/from-addresses` endpoint (registered only when `email.fromAddresses`\n * is set). That route's document id goes unused server-side: the option set is request-scoped, not\n * per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.\n */\nexport const buildFromField = (resolver: FromAddressesResolver): TextField => ({\n\tname: 'from',\n\ttype: 'text',\n\tlabel: labelFor(keys.actionConfigFrom),\n\tvalidate: validateFromField(resolver),\n\tadmin: {\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: FROM_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\tendpoint: 'from-addresses',\n\t\t\t\t\tdescriptionKey: keys.actionConfigFromDescription,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n\nexport type ResolveFromAddressesRequestArgs = {\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: FromAddressesResolver\n}\n\nexport type ResolveFromAddressesRequestResult = {\n\tstatus: number\n\tbody: { options: FromAddressOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:\n * authenticated callers get the host resolver's current options for this request; anonymous\n * callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror\n * the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).\n */\nexport const resolveFromAddressesRequest = async (\n\targs: ResolveFromAddressesRequestArgs\n): Promise<ResolveFromAddressesRequestResult> => {\n\tconst { isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolver({ req })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'From addresses unavailable' }] } }\n\t}\n}\n"],"mappings":";;;AAkBA,MAAM,iBAAiB;;;;;;;;;;;;;;AAevB,MAAa,qBACX,aACD,OAAO,OAAgB,EAAE,UAA2D;CACnF,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAER,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,SAAS,EAAE,IAAI,CAAC;CACjC,QAAQ;EACP,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,yBAAyB;CACzD;CACA,OAAO,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACnD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,qBAAqB;AACjD;;;;;;;AAQD,MAAa,kBAAkB,cAAgD;CAC9E,MAAM;CACN,MAAM;CACN,OAAO,SAAS,KAAK,gBAAgB;CACrC,UAAU,kBAAkB,QAAQ;CACpC,OAAO,EACN,YAAY,EACX,OAAO;EACN,MAAM;EACN,aAAa;GACZ,UAAU;GACV,gBAAgB,KAAK;EACtB;CACD,EACD,EACD;AACD;;;;;;;AAoBA,MAAa,8BAA8B,OAC1C,SACgD;CAChD,MAAM,EAAE,UAAU,KAAK,aAAa;CACpC,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,SAAS,EAAE,IAAI,CAAC,EACA;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,6BAA6B,CAAC,EAAE;EAAE;CACrF;AACD"}
|
|
1
|
+
{"version":3,"file":"fromAddresses.js","names":[],"sources":["../../src/actions/fromAddresses.ts"],"sourcesContent":["import type { PayloadRequest, TextField } from 'payload'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\nimport { isPlausibleEmail } from './emailRecipients'\nimport type { RecipientResolveArgs } from './recipientSources'\n\n/** One selectable \"from\" address for the built-in email actions. */\nexport type FromAddressOption = { label: string; value: string }\n\n/**\n * A sender the plugin resolves server-side at send time (plugin option `email.fromSources`), the\n * from-side counterpart of a `RecipientSource`. `value` is the namespaced string stored on the\n * action (e.g. `tenant:default`), so it cannot collide with a literal address and stays\n * audit-stable while the address it resolves to follows the host; `label` is what the editor sees\n * in the from select. `resolve` returns the address to send from right now (reduced to a single\n * address), or null/empty to send with the email adapter's default sender. A throw fails the\n * action loudly (and retries on the queued path) rather than sending as the wrong identity.\n */\nexport type FromAddressSource = {\n\tvalue: string\n\tlabel: string | Record<string, string>\n\tresolve: (args: RecipientResolveArgs) => Promise<string | null> | string | null\n}\n\nexport type FromAddressSourceRegistry = Record<string, FromAddressSource>\n\n/**\n * The `from` handed to `payload.sendEmail`: a stored source value re-resolves through its source\n * with the run-time args; anything else (a literal picked from `fromAddresses`, which never\n * touches a source at send) is forwarded verbatim, and no configured value means no `from` at\n * all. Mirrors `resolveRecipientEntries`: a throwing source propagates.\n */\nexport const resolveSendFrom = async (opts: {\n\tconfigured: string | undefined\n\tsources?: Map<string, FromAddressSource>\n\tsourceArgs?: RecipientResolveArgs\n}): Promise<string | undefined> => {\n\tconst { configured, sources, sourceArgs } = opts\n\tif (!configured) {\n\t\treturn undefined\n\t}\n\tconst source = sources?.get(configured)\n\tif (!source) {\n\t\treturn configured\n\t}\n\t// A source only resolves with the run-time args; there are none at authoring/validation time.\n\tconst resolved = sourceArgs ? await source.resolve(sourceArgs) : null\n\tif (!resolved) {\n\t\treturn undefined\n\t}\n\treturn firstSender(resolved) || undefined\n}\n\n/** A bare plausible address, or a `Name <addr>` display form wrapping one (quotes and commas in the name included). */\nconst isPlausibleSender = (value: string): boolean => {\n\tif (isPlausibleEmail(value)) {\n\t\treturn true\n\t}\n\tconst bracketed = /^[^<>]*<([^<>\\s]+)>$/.exec(value)\n\treturn Boolean(bracketed?.[1] && isPlausibleEmail(bracketed[1]))\n}\n\n/**\n * The sender-side counterpart of `firstAddress`: one sender only, but `Name <addr>` display form\n * survives because that is the documented shape of a `from`. Order matters: cut at line breaks\n * first (the header-injection vector), accept the whole remaining line so a quoted display name\n * may contain commas, and only then comma-split to clamp a multi-address result to its first\n * entry. An implausible result becomes empty (send with the adapter default) rather than a\n * broken header.\n */\nconst firstSender = (value: string): string => {\n\tconst line = (value.split(/[\\n\\r]+/)[0] ?? '').trim()\n\tif (isPlausibleSender(line)) {\n\t\treturn line\n\t}\n\tconst [first] = line.split(/[,;]+/)\n\tconst cleaned = (first ?? '').trim()\n\treturn isPlausibleSender(cleaned) ? cleaned : ''\n}\n\n/**\n * Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`\n * (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`\n * (host header, cookie, or auth context) and return only that tenant's allowed senders. `value`\n * is the literal string handed to `payload.sendEmail`'s `from` (e.g. `'Name <addr@x.com>'` or a\n * plain address). Absent keeps the email adapter's default sender and adds no `from` field at all.\n */\nexport type FromAddressesResolver = (args: {\n\treq: PayloadRequest\n}) => Promise<FromAddressOption[]> | FromAddressOption[]\n\nconst FROM_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's\n * `toField` and poll's `resultsField`): unset is fine, otherwise the value must be one of the\n * resolver's options for this request. A throwing resolver fails closed with a translated message\n * rather than surfacing a raw error on save.\n *\n * Failing closed has an operational cost worth knowing: Payload runs this on every save, not only\n * when `from` changed, so for as long as the resolver is down no form carrying an email action with\n * a `from` set can be saved at all, including edits that never touch the address. That is the\n * deliberate trade: failing open would persist a sender the host can no longer vouch for, and\n * unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.\n * A resolver reaching a flaky upstream should cache or fall back internally rather than throw.\n */\nexport const validateFromField =\n\t(resolver: FromAddressesResolver | undefined, sourceValues?: Set<string>) =>\n\tasync (value: unknown, { req }: { req: PayloadRequest }): Promise<string | true> => {\n\t\tif (typeof value !== 'string' || value.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\t// A registered source value validates by membership alone, no resolver round trip.\n\t\tif (sourceValues?.has(value)) {\n\t\t\treturn true\n\t\t}\n\t\tif (!resolver) {\n\t\t\treturn asTranslate(req.t)(keys.validationFromUnknown)\n\t\t}\n\t\tlet options: FromAddressOption[]\n\t\ttry {\n\t\t\toptions = await resolver({ req })\n\t\t} catch {\n\t\t\treturn asTranslate(req.t)(keys.validationFromUnavailable)\n\t\t}\n\t\treturn options.some((option) => option.value === value)\n\t\t\t? true\n\t\t\t: asTranslate(req.t)(keys.validationFromUnknown)\n\t}\n\n/**\n * The `from` select shared by `emailTeam` and `confirmation`: an `EndpointOptionsSelect` backed by\n * the forms collection's `/:id/from-addresses` endpoint (registered only when `email.fromAddresses`\n * is set). That route's document id goes unused server-side: the option set is request-scoped, not\n * per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.\n */\nexport const buildFromField = (\n\tresolver: FromAddressesResolver | undefined,\n\tsources?: FromAddressSourceRegistry\n): TextField => ({\n\tname: 'from',\n\ttype: 'text',\n\tlabel: labelFor(keys.actionConfigFrom),\n\tvalidate: validateFromField(\n\t\tresolver,\n\t\tsources ? new Set(Object.values(sources).map((source) => source.value)) : undefined\n\t),\n\tadmin: {\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: FROM_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\tendpoint: 'from-addresses',\n\t\t\t\t\tdescriptionKey: keys.actionConfigFromDescription,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n\nexport type ResolveFromAddressesRequestArgs = {\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver?: FromAddressesResolver\n\tsources?: FromAddressSourceRegistry\n}\n\n/**\n * A source entry as the from select shows it. A string label is display text served raw (matching\n * how `RecipientsSelect` receives source labels); a per-locale record picks the request's admin\n * language, then English, then any value.\n */\nconst sourceOption = (source: FromAddressSource, req: PayloadRequest): FromAddressOption => {\n\tif (typeof source.label === 'string') {\n\t\treturn { label: source.label, value: source.value }\n\t}\n\tconst label =\n\t\tsource.label[req.i18n.language] ??\n\t\tsource.label.en ??\n\t\tObject.values(source.label)[0] ??\n\t\tsource.value\n\treturn { label, value: source.value }\n}\n\nexport type ResolveFromAddressesRequestResult = {\n\tstatus: number\n\tbody: { options: FromAddressOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:\n * authenticated callers get the host resolver's current options for this request; anonymous\n * callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror\n * the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).\n */\nexport const resolveFromAddressesRequest = async (\n\targs: ResolveFromAddressesRequestArgs\n): Promise<ResolveFromAddressesRequestResult> => {\n\tconst { isAuthed, req, resolver, sources } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\ttry {\n\t\t// Sources lead: the send-time-resolved sender is the tenant identity, static literals are\n\t\t// the exceptions an editor picks deliberately.\n\t\tconst sourceOptions = Object.values(sources ?? {}).map((source) => sourceOption(source, req))\n\t\tconst resolved = resolver ? await resolver({ req }) : []\n\t\treturn { status: 200, body: { options: [...sourceOptions, ...resolved] } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'From addresses unavailable' }] } }\n\t}\n}\n"],"mappings":";;;;;;;;;;AAgCA,MAAa,kBAAkB,OAAO,SAIH;CAClC,MAAM,EAAE,YAAY,SAAS,eAAe;CAC5C,IAAI,CAAC,YACJ;CAED,MAAM,SAAS,SAAS,IAAI,UAAU;CACtC,IAAI,CAAC,QACJ,OAAO;CAGR,MAAM,WAAW,aAAa,MAAM,OAAO,QAAQ,UAAU,IAAI;CACjE,IAAI,CAAC,UACJ;CAED,OAAO,YAAY,QAAQ,KAAK,KAAA;AACjC;;AAGA,MAAM,qBAAqB,UAA2B;CACrD,IAAI,iBAAiB,KAAK,GACzB,OAAO;CAER,MAAM,YAAY,uBAAuB,KAAK,KAAK;CACnD,OAAO,QAAQ,YAAY,MAAM,iBAAiB,UAAU,EAAE,CAAC;AAChE;;;;;;;;;AAUA,MAAM,eAAe,UAA0B;CAC9C,MAAM,QAAQ,MAAM,MAAM,SAAS,EAAE,MAAM,IAAI,KAAK;CACpD,IAAI,kBAAkB,IAAI,GACzB,OAAO;CAER,MAAM,CAAC,SAAS,KAAK,MAAM,OAAO;CAClC,MAAM,WAAW,SAAS,IAAI,KAAK;CACnC,OAAO,kBAAkB,OAAO,IAAI,UAAU;AAC/C;AAaA,MAAM,iBAAiB;;;;;;;;;;;;;;AAevB,MAAa,qBACX,UAA6C,iBAC9C,OAAO,OAAgB,EAAE,UAA2D;CACnF,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAGR,IAAI,cAAc,IAAI,KAAK,GAC1B,OAAO;CAER,IAAI,CAAC,UACJ,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,qBAAqB;CAErD,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,SAAS,EAAE,IAAI,CAAC;CACjC,QAAQ;EACP,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,yBAAyB;CACzD;CACA,OAAO,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACnD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,qBAAqB;AACjD;;;;;;;AAQD,MAAa,kBACZ,UACA,aACgB;CAChB,MAAM;CACN,MAAM;CACN,OAAO,SAAS,KAAK,gBAAgB;CACrC,UAAU,kBACT,UACA,UAAU,IAAI,IAAI,OAAO,OAAO,OAAO,EAAE,KAAK,WAAW,OAAO,KAAK,CAAC,IAAI,KAAA,CAC3E;CACA,OAAO,EACN,YAAY,EACX,OAAO;EACN,MAAM;EACN,aAAa;GACZ,UAAU;GACV,gBAAgB,KAAK;EACtB;CACD,EACD,EACD;AACD;;;;;;AAeA,MAAM,gBAAgB,QAA2B,QAA2C;CAC3F,IAAI,OAAO,OAAO,UAAU,UAC3B,OAAO;EAAE,OAAO,OAAO;EAAO,OAAO,OAAO;CAAM;CAOnD,OAAO;EAAE,OAJR,OAAO,MAAM,IAAI,KAAK,aACtB,OAAO,MAAM,MACb,OAAO,OAAO,OAAO,KAAK,EAAE,MAC5B,OAAO;EACQ,OAAO,OAAO;CAAM;AACrC;;;;;;;AAaA,MAAa,8BAA8B,OAC1C,SACgD;CAChD,MAAM,EAAE,UAAU,KAAK,UAAU,YAAY;CAC7C,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAGH,MAAM,gBAAgB,OAAO,OAAO,WAAW,CAAC,CAAC,EAAE,KAAK,WAAW,aAAa,QAAQ,GAAG,CAAC;EAC5F,MAAM,WAAW,WAAW,MAAM,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC;EACvD,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAS,CAAC,GAAG,eAAe,GAAG,QAAQ,EAAE;EAAE;CAC1E,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,6BAA6B,CAAC,EAAE;EAAE;CACrF;AACD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/actions/recipientSources.ts
|
|
2
|
-
/** Index
|
|
2
|
+
/** Index a source registry (recipient or from) by stored `value` for O(1) lookup during validation and resolution. */
|
|
3
3
|
const sourcesByValue = (registry) => {
|
|
4
4
|
const map = /* @__PURE__ */ new Map();
|
|
5
5
|
for (const source of Object.values(registry ?? {})) map.set(source.value, source);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipientSources.js","names":[],"sources":["../../src/actions/recipientSources.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport type { FormContextReference } from '../context/formContext'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\n\n/** Arguments a recipient source's `resolve` receives when a submission's email actions run. */\nexport type RecipientResolveArgs = {\n\t/** The verified form-context reference, or null when the form was rendered without one. */\n\tcontext: FormContextReference | null\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: { id: number | string; title?: string }\n\tsubmissionId: number | string\n\tpayload: Payload\n\treq?: PayloadRequest\n\tlocale: string\n}\n\n/**\n * A recipient the plugin resolves server-side at send time (plugin option `email.recipientSources`).\n * `value` is the namespaced string stored in the recipient list (e.g. `context:pageContact`), so it\n * cannot collide with an address; `label` is what the editor sees in the recipient field. `resolve`\n * returns zero or more addresses, `[]` meaning \"nothing to send to from this source\", which is normal.\n */\nexport type RecipientSource = {\n\tvalue: string\n\tlabel: string | Record<string, string>\n\tresolve: (args: RecipientResolveArgs) => Promise<string[]> | string[]\n}\n\nexport type RecipientSourceRegistry = Record<string, RecipientSource>\n\n/** Index
|
|
1
|
+
{"version":3,"file":"recipientSources.js","names":[],"sources":["../../src/actions/recipientSources.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport type { FormContextReference } from '../context/formContext'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\n\n/** Arguments a recipient source's `resolve` receives when a submission's email actions run. */\nexport type RecipientResolveArgs = {\n\t/** The verified form-context reference, or null when the form was rendered without one. */\n\tcontext: FormContextReference | null\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: { id: number | string; title?: string }\n\tsubmissionId: number | string\n\tpayload: Payload\n\treq?: PayloadRequest\n\tlocale: string\n}\n\n/**\n * A recipient the plugin resolves server-side at send time (plugin option `email.recipientSources`).\n * `value` is the namespaced string stored in the recipient list (e.g. `context:pageContact`), so it\n * cannot collide with an address; `label` is what the editor sees in the recipient field. `resolve`\n * returns zero or more addresses, `[]` meaning \"nothing to send to from this source\", which is normal.\n */\nexport type RecipientSource = {\n\tvalue: string\n\tlabel: string | Record<string, string>\n\tresolve: (args: RecipientResolveArgs) => Promise<string[]> | string[]\n}\n\nexport type RecipientSourceRegistry = Record<string, RecipientSource>\n\n/** Index a source registry (recipient or from) by stored `value` for O(1) lookup during validation and resolution. */\nexport const sourcesByValue = <T extends { value: string }>(\n\tregistry?: Record<string, T>\n): Map<string, T> => {\n\tconst map = new Map<string, T>()\n\tfor (const source of Object.values(registry ?? {})) {\n\t\tmap.set(source.value, source)\n\t}\n\treturn map\n}\n"],"mappings":";;AAgCA,MAAa,kBACZ,aACoB;CACpB,MAAM,sBAAM,IAAI,IAAe;CAC/B,KAAK,MAAM,UAAU,OAAO,OAAO,YAAY,CAAC,CAAC,GAChD,IAAI,IAAI,OAAO,OAAO,MAAM;CAE7B,OAAO;AACR"}
|
|
@@ -90,7 +90,7 @@ const stampFileCollections = (rows, slug) => {
|
|
|
90
90
|
if (Array.isArray(subFields)) stampFileCollections(subFields, slug);
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
|
-
const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed, calcSources, consentSources, consentResolveOnRead, actionRegistry = /* @__PURE__ */ new Map(), localizeContent = true, richText, uploadsCollectionSlug, resultsAccess, pollVotesEnabled = false, pollSourceRegistry, pollTypeRegistry, outcomeFields, buttons, settings, response, fromAddresses, departments, redirectRelationships }) => {
|
|
93
|
+
const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed, calcSources, consentSources, consentResolveOnRead, actionRegistry = /* @__PURE__ */ new Map(), localizeContent = true, richText, uploadsCollectionSlug, resultsAccess, pollVotesEnabled = false, pollSourceRegistry, pollTypeRegistry, outcomeFields, buttons, settings, response, fromAddresses, fromSources, departments, redirectRelationships }) => {
|
|
94
94
|
const conditionTypes = buildConditionTypeMap(registry);
|
|
95
95
|
const pollTypes = pollTypeRegistry ?? resolvePollTypes();
|
|
96
96
|
const pollResultsTypes = pollEligibleTypes(registry);
|
|
@@ -487,6 +487,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, calcAllowed,
|
|
|
487
487
|
pollVotesEnabled,
|
|
488
488
|
consentSources,
|
|
489
489
|
fromAddresses,
|
|
490
|
+
fromSources,
|
|
490
491
|
departments
|
|
491
492
|
});
|
|
492
493
|
return {
|