@aglyn/plugins-email 1.0.0-beta.221 → 1.0.0-beta.222

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.
Files changed (82) hide show
  1. package/README.md +5 -4
  2. package/package.json +16 -17
  3. package/src/lib/components/email-template-detail.d.ts +5 -13
  4. package/src/lib/components/email-template-detail.js +29 -426
  5. package/src/lib/components/email-template-detail.js.map +1 -1
  6. package/src/lib/components/email-zones.d.ts +15 -0
  7. package/src/lib/components/email-zones.js +1 -0
  8. package/src/lib/components/email-zones.js.map +1 -1
  9. package/src/lib/components/list-detail-card.d.ts +0 -36
  10. package/src/lib/components/list-detail-card.js +125 -84
  11. package/src/lib/components/list-detail-card.js.map +1 -1
  12. package/src/lib/components/list-members-panel.d.ts +17 -0
  13. package/src/lib/components/list-members-panel.js +14 -0
  14. package/src/lib/components/list-members-panel.js.map +1 -1
  15. package/src/lib/components/site-package-email-preview.d.ts +13 -0
  16. package/src/lib/components/site-package-email-preview.js +47 -0
  17. package/src/lib/components/site-package-email-preview.js.map +1 -0
  18. package/src/lib/components/suppressions-card.js +75 -6
  19. package/src/lib/components/suppressions-card.js.map +1 -1
  20. package/src/lib/components/use-org-email-topics.js +2 -9
  21. package/src/lib/components/use-org-email-topics.js.map +1 -1
  22. package/src/lib/constants/site-package.d.ts +21 -0
  23. package/src/lib/constants/site-package.js +21 -0
  24. package/src/lib/constants/site-package.js.map +1 -0
  25. package/src/lib/declarations.console-server.d.ts +12 -0
  26. package/src/lib/declarations.console-server.js +73 -0
  27. package/src/lib/declarations.console-server.js.map +1 -1
  28. package/src/lib/list-import.d.ts +64 -75
  29. package/src/lib/list-import.js +43 -158
  30. package/src/lib/list-import.js.map +1 -1
  31. package/src/lib/model/email-topic-catalog.d.ts +24 -0
  32. package/src/lib/model/email-topic-catalog.js +19 -0
  33. package/src/lib/model/email-topic-catalog.js.map +1 -1
  34. package/src/lib/model/index.d.ts +0 -25
  35. package/src/lib/model/index.js +0 -27
  36. package/src/lib/model/index.js.map +1 -1
  37. package/src/lib/plugin.d.ts +8 -0
  38. package/src/lib/plugin.js +76 -2
  39. package/src/lib/plugin.js.map +1 -1
  40. package/src/lib/server/person-eraser.d.ts +39 -0
  41. package/src/lib/server/person-eraser.js +44 -0
  42. package/src/lib/server/person-eraser.js.map +1 -0
  43. package/src/lib/server-console.js +9 -9
  44. package/src/lib/server-console.js.map +1 -1
  45. package/src/lib/server-list-gate.d.ts +7 -0
  46. package/src/lib/server-list-gate.js +10 -3
  47. package/src/lib/server-list-gate.js.map +1 -1
  48. package/src/lib/server-list-import.d.ts +20 -177
  49. package/src/lib/server-list-import.js +34 -594
  50. package/src/lib/server-list-import.js.map +1 -1
  51. package/src/lib/server.js +16 -29
  52. package/src/lib/server.js.map +1 -1
  53. package/src/lib/testing/stand-in-send-tally.d.ts +29 -0
  54. package/src/lib/testing/stand-in-send-tally.js +52 -0
  55. package/src/lib/testing/stand-in-send-tally.js.map +1 -0
  56. package/src/lib/transfer/email-transfer-catalog.d.ts +151 -0
  57. package/src/lib/transfer/email-transfer-catalog.js +536 -0
  58. package/src/lib/transfer/email-transfer-catalog.js.map +1 -0
  59. package/src/lib/transfer/list-import-steps.d.ts +44 -0
  60. package/src/lib/transfer/list-import-steps.js +201 -0
  61. package/src/lib/transfer/list-import-steps.js.map +1 -0
  62. package/src/lib/transfer/list-members.server.d.ts +165 -0
  63. package/src/lib/transfer/list-members.server.js +911 -0
  64. package/src/lib/transfer/list-members.server.js.map +1 -0
  65. package/src/lib/transfer/suppressions.server.d.ts +69 -0
  66. package/src/lib/transfer/suppressions.server.js +282 -0
  67. package/src/lib/transfer/suppressions.server.js.map +1 -0
  68. package/src/lib/transfer/topics-package.d.ts +56 -0
  69. package/src/lib/transfer/topics-package.js +87 -0
  70. package/src/lib/transfer/topics-package.js.map +1 -0
  71. package/src/lib/transfer/topics-package.server.d.ts +33 -0
  72. package/src/lib/transfer/topics-package.server.js +171 -0
  73. package/src/lib/transfer/topics-package.server.js.map +1 -0
  74. package/src/lib/transfer/transfer-reads.server.d.ts +25 -0
  75. package/src/lib/transfer/transfer-reads.server.js +34 -0
  76. package/src/lib/transfer/transfer-reads.server.js.map +1 -0
  77. package/src/lib/components/list-import-drawer.d.ts +0 -22
  78. package/src/lib/components/list-import-drawer.js +0 -662
  79. package/src/lib/components/list-import-drawer.js.map +0 -1
  80. package/src/lib/model/template-report.d.ts +0 -158
  81. package/src/lib/model/template-report.js +0 -249
  82. package/src/lib/model/template-report.js.map +0 -1
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * WHAT THE EMAIL PLUGIN IMPORTS AND EXPORTS (AGL-3529) — the half both the
19
+ * server and the console read: the resource keys, every field, the match
20
+ * keys, the header spellings, the locked rules and the answers of the
21
+ * plugin's own wizard steps.
22
+ *
23
+ * Two resources, both a site's:
24
+ *
25
+ * - `email.list-members` — one list's members, one list at a time
26
+ * (`instances`: `email.list-members:<listId>`): an import is INTO one list
27
+ * and an export is OF one, and the console opens both from the list's page.
28
+ * - `email.suppressions` — the site's own suppression list. An import only
29
+ * ever ADDS: an address already on the list is left exactly as it is, and
30
+ * nothing — the import, its undo — ever removes one.
31
+ *
32
+ * Types only from the transfer core, so the console registrar that reads
33
+ * this costs no transfer code.
34
+ */
35
+ import type { MatchKeySpec, TransferAliasDictionary, TransferCatalogInput, TransferField, TransferLockedRule } from '@aglyn/aglyn/data-transfer';
36
+ /**
37
+ * One list's members, imported into and exported from that list: declared
38
+ * with `instances`, and reached per list as `email.list-members:<listId>`, so
39
+ * a job, a person's remembered export fields and the one-running-import rule
40
+ * each belong to one list.
41
+ */
42
+ export declare const LIST_MEMBERS_RESOURCE = "email.list-members";
43
+ /**
44
+ * The resource key one list's members move under — the core's instance key
45
+ * (`transferResourceInstanceKey`), spelled out so the list's page names it
46
+ * without loading the transfer core; `email-transfer-catalog.spec.ts` holds
47
+ * the two to the same answer.
48
+ */
49
+ export declare function listMembersResourceKey(listId: string): string;
50
+ /** The list a list-member resource key names, or `null` for any other key. */
51
+ export declare function listIdOfResourceKey(key: string): string | null;
52
+ /** A site's suppression list. */
53
+ export declare const SUPPRESSIONS_RESOURCE = "email.suppressions";
54
+ /**
55
+ * "People already on this list" — after Matching: whether the file may
56
+ * change the contact details of people the workspace already holds.
57
+ */
58
+ export declare const LIST_EXISTING_STEP_ID = "email-existing";
59
+ /** "Permission" — after Conflicts: the statement of permission, with the screening. */
60
+ export declare const LIST_CONSENT_STEP_ID = "email-consent";
61
+ /** The answer of the {@link LIST_EXISTING_STEP_ID} step. */
62
+ export interface ListExistingAnswer {
63
+ /**
64
+ * The file's contact columns may change the details of a person the
65
+ * workspace already holds, under the field choices made in Conflicts.
66
+ * Otherwise they fill only a contact the import creates, and somebody
67
+ * already on the list is left exactly as they are.
68
+ */
69
+ updateContacts: boolean;
70
+ }
71
+ /** The answer of the {@link LIST_CONSENT_STEP_ID} step. */
72
+ export interface ListConsentAnswer {
73
+ /**
74
+ * The operator states they have permission to email everyone in the file.
75
+ * Without it only people with an opt-in on record are added.
76
+ */
77
+ attest: boolean;
78
+ }
79
+ /**
80
+ * The existing-people answer in `extras`, or the default — leave everybody
81
+ * the workspace holds as they are. Anything but a literal `true` is no.
82
+ */
83
+ export declare function readListExistingAnswer(extras: Readonly<Record<string, unknown>> | undefined): ListExistingAnswer;
84
+ /**
85
+ * The permission answer in `extras`, or `null` when the step was never
86
+ * answered. Anything but a literal `true` is no statement at all.
87
+ */
88
+ export declare function readListConsentAnswer(extras: Readonly<Record<string, unknown>> | undefined): ListConsentAnswer | null;
89
+ /** What the screening found, as the list's import ledger keeps it and the permission step shows it. */
90
+ export interface ListImportScreeningReport {
91
+ roleAccounts: number;
92
+ roleAccountSamples: string[];
93
+ purchaseTellColumns: string[];
94
+ declaresBasis: boolean;
95
+ /** The consent gate over the first new addresses — a sample, with its size. */
96
+ sample: {
97
+ size: number;
98
+ of: number;
99
+ optedIn: number;
100
+ needAttestation: number;
101
+ refused: number;
102
+ };
103
+ }
104
+ /** A contact-record column's field id: `contact:<the record's own field name>`. */
105
+ export declare const CONTACT_FIELD_PREFIX = "contact:";
106
+ /**
107
+ * The contact record's fields a list file may carry, by the record's own
108
+ * names — what the contact-capture door's `profile` takes. Written through
109
+ * that door and never by this plugin (see `list-members.server.ts`).
110
+ */
111
+ export declare const LIST_MEMBER_CONTACT_FIELDS: ReadonlyArray<{
112
+ key: string;
113
+ label: string;
114
+ type: TransferField['type'];
115
+ aliases: readonly string[];
116
+ }>;
117
+ /** The field id of a contact-record column. */
118
+ export declare function contactFieldId(key: string): string;
119
+ /** The contact record's own name for a field id, or `null` for a membership field. */
120
+ export declare function contactFieldKey(fieldId: string): string | null;
121
+ /** Every contact-record field id, in catalog order. */
122
+ export declare const LIST_MEMBER_CONTACT_FIELD_IDS: readonly string[];
123
+ /** The membership fields a file may write on a NEW member. */
124
+ export declare const LIST_MEMBER_WRITTEN_FIELDS: readonly ["email", "name", "declaredSource", "declaredAt"];
125
+ /** One list's members: every field, for the wizard and the export dialog. */
126
+ export declare function listMemberCatalog(): TransferCatalogInput;
127
+ /** A row finds its member by address, or by the member id an export carries. */
128
+ export declare const LIST_MEMBER_MATCH_KEYS: readonly MatchKeySpec[];
129
+ /**
130
+ * Other products' list-export headers. Spellings only — every one of them
131
+ * is read the same way as the platform's own.
132
+ */
133
+ export declare const LIST_MEMBER_ALIASES: readonly TransferAliasDictionary[];
134
+ /** Why somebody already on the list keeps their membership as it is. */
135
+ export declare const MEMBERSHIP_KEPT_REASON = "Somebody already on this list keeps their membership as it is: an import never re-enrolls them or rewrites what their membership records.";
136
+ /** Why a contact detail is never emptied by a file. */
137
+ export declare const CONTACT_NEVER_CLEARED_REASON = "An import adds and replaces contact details; a blank cell never empties one.";
138
+ /** Why the file holds no say over consent. */
139
+ export declare const CONSENT_FROM_FILE_REASON = "A file never says who agreed to hear from you. People with an opt-in on record keep it; anybody else is added only on your statement of permission, which is stored against your account; people who unsubscribed, bounced, complained or said no are never added.";
140
+ /** The list-member import's rules, shown locked in the wizard with their reasons. */
141
+ export declare const LIST_MEMBER_LOCKED_RULES: readonly TransferLockedRule[];
142
+ /** Why an entry already on the list is left alone. */
143
+ export declare const SUPPRESSION_KEPT_REASON = "An address already suppressed keeps its entry exactly as it is \u2014 the reason and date it was suppressed with are the record of why \u2014 and an import never removes one.";
144
+ /** A site's suppression list: every field. */
145
+ export declare function suppressionCatalog(): TransferCatalogInput;
146
+ /** A suppression is the address. */
147
+ export declare const SUPPRESSION_MATCH_KEYS: readonly MatchKeySpec[];
148
+ /** The suppression import's rule: nothing already on the list is changed. */
149
+ export declare const SUPPRESSION_LOCKED_RULES: readonly TransferLockedRule[];
150
+ /** Other products' suppression-export headers. */
151
+ export declare const SUPPRESSION_ALIASES: readonly TransferAliasDictionary[];
@@ -0,0 +1,536 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * WHAT THE EMAIL PLUGIN IMPORTS AND EXPORTS (AGL-3529) — the half both the
18
+ * server and the console read: the resource keys, every field, the match
19
+ * keys, the header spellings, the locked rules and the answers of the
20
+ * plugin's own wizard steps.
21
+ *
22
+ * Two resources, both a site's:
23
+ *
24
+ * - `email.list-members` — one list's members, one list at a time
25
+ * (`instances`: `email.list-members:<listId>`): an import is INTO one list
26
+ * and an export is OF one, and the console opens both from the list's page.
27
+ * - `email.suppressions` — the site's own suppression list. An import only
28
+ * ever ADDS: an address already on the list is left exactly as it is, and
29
+ * nothing — the import, its undo — ever removes one.
30
+ *
31
+ * Types only from the transfer core, so the console registrar that reads
32
+ * this costs no transfer code.
33
+ */ import { LIST_IMPORT_EMAIL_COLUMNS, LIST_IMPORT_NAME_COLUMNS, LIST_IMPORT_OPT_IN_DATE_COLUMNS, LIST_IMPORT_OPT_IN_SOURCE_COLUMNS } from "../list-import.js";
34
+ /**
35
+ * One list's members, imported into and exported from that list: declared
36
+ * with `instances`, and reached per list as `email.list-members:<listId>`, so
37
+ * a job, a person's remembered export fields and the one-running-import rule
38
+ * each belong to one list.
39
+ */ export const LIST_MEMBERS_RESOURCE = 'email.list-members';
40
+ /**
41
+ * The resource key one list's members move under — the core's instance key
42
+ * (`transferResourceInstanceKey`), spelled out so the list's page names it
43
+ * without loading the transfer core; `email-transfer-catalog.spec.ts` holds
44
+ * the two to the same answer.
45
+ */ export function listMembersResourceKey(listId) {
46
+ return `${LIST_MEMBERS_RESOURCE}:${listId}`;
47
+ }
48
+ /** The list a list-member resource key names, or `null` for any other key. */ export function listIdOfResourceKey(key) {
49
+ const prefix = `${LIST_MEMBERS_RESOURCE}:`;
50
+ const listId = String(key != null ? key : '').startsWith(prefix) ? String(key).slice(prefix.length) : '';
51
+ return /^[A-Za-z0-9_-]{1,128}$/.test(listId) ? listId : null;
52
+ }
53
+ /** A site's suppression list. */ export const SUPPRESSIONS_RESOURCE = 'email.suppressions';
54
+ /*==========================================
55
+ * THE PLUGIN'S OWN WIZARD STEPS
56
+ *=========================================*/ /**
57
+ * "People already on this list" — after Matching: whether the file may
58
+ * change the contact details of people the workspace already holds.
59
+ */ export const LIST_EXISTING_STEP_ID = 'email-existing';
60
+ /** "Permission" — after Conflicts: the statement of permission, with the screening. */ export const LIST_CONSENT_STEP_ID = 'email-consent';
61
+ /**
62
+ * The existing-people answer in `extras`, or the default — leave everybody
63
+ * the workspace holds as they are. Anything but a literal `true` is no.
64
+ */ export function readListExistingAnswer(extras) {
65
+ const value = extras == null ? void 0 : extras[LIST_EXISTING_STEP_ID];
66
+ return {
67
+ updateContacts: !!value && typeof value === 'object' && value['updateContacts'] === true
68
+ };
69
+ }
70
+ /**
71
+ * The permission answer in `extras`, or `null` when the step was never
72
+ * answered. Anything but a literal `true` is no statement at all.
73
+ */ export function readListConsentAnswer(extras) {
74
+ const value = extras == null ? void 0 : extras[LIST_CONSENT_STEP_ID];
75
+ if (!value || typeof value !== 'object') return null;
76
+ const attest = value['attest'];
77
+ return typeof attest === 'boolean' ? {
78
+ attest
79
+ } : null;
80
+ }
81
+ /*==========================================
82
+ * LIST MEMBERS
83
+ *=========================================*/ /** A contact-record column's field id: `contact:<the record's own field name>`. */ export const CONTACT_FIELD_PREFIX = 'contact:';
84
+ /**
85
+ * The contact record's fields a list file may carry, by the record's own
86
+ * names — what the contact-capture door's `profile` takes. Written through
87
+ * that door and never by this plugin (see `list-members.server.ts`).
88
+ */ export const LIST_MEMBER_CONTACT_FIELDS = [
89
+ {
90
+ key: 'firstName',
91
+ label: 'First name',
92
+ type: 'text',
93
+ aliases: [
94
+ 'first name',
95
+ 'firstname',
96
+ 'given name',
97
+ 'fname',
98
+ 'FNAME'
99
+ ]
100
+ },
101
+ {
102
+ key: 'lastName',
103
+ label: 'Last name',
104
+ type: 'text',
105
+ aliases: [
106
+ 'last name',
107
+ 'lastname',
108
+ 'surname',
109
+ 'family name',
110
+ 'lname',
111
+ 'LNAME'
112
+ ]
113
+ },
114
+ {
115
+ key: 'phone',
116
+ label: 'Phone',
117
+ type: 'phone',
118
+ aliases: [
119
+ 'phone',
120
+ 'phone number',
121
+ 'telephone',
122
+ 'PHONE'
123
+ ]
124
+ },
125
+ {
126
+ key: 'mobilePhone',
127
+ label: 'Mobile phone',
128
+ type: 'phone',
129
+ aliases: [
130
+ 'mobile',
131
+ 'mobile phone',
132
+ 'cell',
133
+ 'cell phone'
134
+ ]
135
+ },
136
+ {
137
+ key: 'jobTitle',
138
+ label: 'Job title',
139
+ type: 'text',
140
+ aliases: [
141
+ 'title',
142
+ 'job title',
143
+ 'position',
144
+ 'role'
145
+ ]
146
+ },
147
+ {
148
+ key: 'companyName',
149
+ label: 'Company',
150
+ type: 'text',
151
+ aliases: [
152
+ 'company',
153
+ 'company name',
154
+ 'organization',
155
+ 'employer'
156
+ ]
157
+ },
158
+ {
159
+ key: 'department',
160
+ label: 'Department',
161
+ type: 'text',
162
+ aliases: [
163
+ 'department',
164
+ 'team'
165
+ ]
166
+ }
167
+ ];
168
+ /** The field id of a contact-record column. */ export function contactFieldId(key) {
169
+ return `${CONTACT_FIELD_PREFIX}${key}`;
170
+ }
171
+ /** The contact record's own name for a field id, or `null` for a membership field. */ export function contactFieldKey(fieldId) {
172
+ return fieldId.startsWith(CONTACT_FIELD_PREFIX) ? fieldId.slice(CONTACT_FIELD_PREFIX.length) : null;
173
+ }
174
+ /** Every contact-record field id, in catalog order. */ export const LIST_MEMBER_CONTACT_FIELD_IDS = LIST_MEMBER_CONTACT_FIELDS.map((field)=>contactFieldId(field.key));
175
+ /** The membership fields a file may write on a NEW member. */ export const LIST_MEMBER_WRITTEN_FIELDS = [
176
+ 'email',
177
+ 'name',
178
+ 'declaredSource',
179
+ 'declaredAt'
180
+ ];
181
+ /** The groups, in picker order. */ const LIST_MEMBER_GROUPS = [
182
+ {
183
+ id: 'membership',
184
+ label: 'Membership'
185
+ },
186
+ {
187
+ id: 'evidence',
188
+ label: 'Where the address came from'
189
+ },
190
+ {
191
+ id: 'consent',
192
+ label: 'Consent'
193
+ },
194
+ {
195
+ id: 'contact',
196
+ label: 'Contact record'
197
+ }
198
+ ];
199
+ /** One list's members: every field, for the wizard and the export dialog. */ export function listMemberCatalog() {
200
+ return {
201
+ groups: LIST_MEMBER_GROUPS,
202
+ standard: [
203
+ {
204
+ id: 'email',
205
+ label: 'Email',
206
+ group: 'membership',
207
+ type: 'email',
208
+ required: true,
209
+ matchKey: true,
210
+ aliases: LIST_IMPORT_EMAIL_COLUMNS,
211
+ description: 'Who the member is. A person is on a list once, by address.'
212
+ },
213
+ {
214
+ id: 'name',
215
+ label: 'Name',
216
+ group: 'membership',
217
+ type: 'text',
218
+ maxLength: 120,
219
+ aliases: LIST_IMPORT_NAME_COLUMNS,
220
+ description: 'The name the list shows. Composed from the first and last name when only those are mapped.'
221
+ },
222
+ {
223
+ id: 'declaredSource',
224
+ label: 'Opt-in source',
225
+ group: 'evidence',
226
+ type: 'text',
227
+ maxLength: 200,
228
+ aliases: LIST_IMPORT_OPT_IN_SOURCE_COLUMNS,
229
+ description: 'Where your file says the person signed up. Kept with your statement of permission, never as their own opt-in.'
230
+ },
231
+ {
232
+ id: 'declaredAt',
233
+ label: 'Opt-in date',
234
+ group: 'evidence',
235
+ type: 'text',
236
+ maxLength: 60,
237
+ aliases: LIST_IMPORT_OPT_IN_DATE_COLUMNS,
238
+ description: 'When your file says the person signed up, kept as written.'
239
+ },
240
+ {
241
+ id: 'addedAt',
242
+ label: 'Added',
243
+ group: 'membership',
244
+ type: 'datetime',
245
+ readOnly: true
246
+ },
247
+ {
248
+ id: 'source',
249
+ label: 'Added through',
250
+ group: 'membership',
251
+ type: 'text',
252
+ readOnly: true
253
+ },
254
+ {
255
+ id: 'via',
256
+ label: 'How',
257
+ group: 'membership',
258
+ type: 'text',
259
+ readOnly: true,
260
+ description: '`rule` for a live list’s match, `manual` for everyone added another way.'
261
+ },
262
+ {
263
+ id: 'consent',
264
+ label: 'Consent',
265
+ group: 'consent',
266
+ type: 'text',
267
+ readOnly: true,
268
+ description: '`granted`, `declined` or `unrecorded`, as the site the list is worked as reads it.'
269
+ },
270
+ {
271
+ id: 'consentBasis',
272
+ label: 'Consent basis',
273
+ group: 'consent',
274
+ type: 'text',
275
+ readOnly: true,
276
+ description: '`contact-opt-in` when the person opted in, `operator-attested` when a member of your team stated permission.'
277
+ },
278
+ {
279
+ id: 'consentAt',
280
+ label: 'Consent recorded',
281
+ group: 'consent',
282
+ type: 'datetime',
283
+ readOnly: true
284
+ },
285
+ {
286
+ id: 'consentReason',
287
+ label: 'Consent note',
288
+ group: 'consent',
289
+ type: 'longText',
290
+ readOnly: true
291
+ },
292
+ ...LIST_MEMBER_CONTACT_FIELDS.map((field)=>({
293
+ id: contactFieldId(field.key),
294
+ label: field.label,
295
+ group: 'contact',
296
+ type: field.type,
297
+ aliases: field.aliases,
298
+ maxLength: 120
299
+ }))
300
+ ]
301
+ };
302
+ }
303
+ /** A row finds its member by address, or by the member id an export carries. */ export const LIST_MEMBER_MATCH_KEYS = [
304
+ {
305
+ fieldId: 'email',
306
+ normalizer: 'email'
307
+ },
308
+ {
309
+ fieldId: 'id',
310
+ normalizer: 'aglynId'
311
+ }
312
+ ];
313
+ /**
314
+ * Other products' list-export headers. Spellings only — every one of them
315
+ * is read the same way as the platform's own.
316
+ */ export const LIST_MEMBER_ALIASES = [
317
+ {
318
+ source: 'Mailchimp audience export',
319
+ aliases: {
320
+ email: [
321
+ 'Email Address'
322
+ ],
323
+ [contactFieldId('firstName')]: [
324
+ 'First Name'
325
+ ],
326
+ [contactFieldId('lastName')]: [
327
+ 'Last Name'
328
+ ],
329
+ [contactFieldId('phone')]: [
330
+ 'Phone Number'
331
+ ],
332
+ declaredAt: [
333
+ 'OPTIN_TIME',
334
+ 'CONFIRM_TIME'
335
+ ]
336
+ }
337
+ },
338
+ {
339
+ source: 'Klaviyo list export',
340
+ aliases: {
341
+ email: [
342
+ 'Email'
343
+ ],
344
+ [contactFieldId('firstName')]: [
345
+ 'First Name'
346
+ ],
347
+ [contactFieldId('lastName')]: [
348
+ 'Last Name'
349
+ ],
350
+ [contactFieldId('phone')]: [
351
+ 'Phone Number'
352
+ ],
353
+ [contactFieldId('companyName')]: [
354
+ 'Organization'
355
+ ],
356
+ [contactFieldId('jobTitle')]: [
357
+ 'Title'
358
+ ],
359
+ declaredSource: [
360
+ 'Source',
361
+ 'List Source'
362
+ ],
363
+ declaredAt: [
364
+ 'Date Added',
365
+ 'Consent Timestamp'
366
+ ]
367
+ }
368
+ },
369
+ {
370
+ source: 'Constant Contact export',
371
+ aliases: {
372
+ email: [
373
+ 'Email address'
374
+ ],
375
+ [contactFieldId('firstName')]: [
376
+ 'First name'
377
+ ],
378
+ [contactFieldId('lastName')]: [
379
+ 'Last name'
380
+ ],
381
+ [contactFieldId('companyName')]: [
382
+ 'Company name'
383
+ ],
384
+ [contactFieldId('jobTitle')]: [
385
+ 'Job title'
386
+ ],
387
+ [contactFieldId('mobilePhone')]: [
388
+ 'Phone - mobile'
389
+ ],
390
+ declaredSource: [
391
+ 'Source name'
392
+ ],
393
+ declaredAt: [
394
+ 'Email permission date'
395
+ ]
396
+ }
397
+ }
398
+ ];
399
+ /** Why somebody already on the list keeps their membership as it is. */ export const MEMBERSHIP_KEPT_REASON = 'Somebody already on this list keeps their membership as it is: an import never re-enrolls them or rewrites what their membership records.';
400
+ /** Why a contact detail is never emptied by a file. */ export const CONTACT_NEVER_CLEARED_REASON = 'An import adds and replaces contact details; a blank cell never empties one.';
401
+ /** Why the file holds no say over consent. */ export const CONSENT_FROM_FILE_REASON = 'A file never says who agreed to hear from you. People with an opt-in on record keep it; anybody else is added only on your statement of permission, which is stored against your account; people who unsubscribed, bounced, complained or said no are never added.';
402
+ /** The list-member import's rules, shown locked in the wizard with their reasons. */ export const LIST_MEMBER_LOCKED_RULES = [
403
+ {
404
+ fieldId: 'email',
405
+ forced: {
406
+ mode: 'keepExisting'
407
+ },
408
+ reason: 'A member is their address: a row matched by its ID never moves the member to another one.'
409
+ },
410
+ ...[
411
+ 'name',
412
+ 'declaredSource',
413
+ 'declaredAt'
414
+ ].map((fieldId)=>({
415
+ fieldId,
416
+ forced: {
417
+ mode: 'keepExisting'
418
+ },
419
+ reason: MEMBERSHIP_KEPT_REASON
420
+ })),
421
+ {
422
+ fieldId: 'consent',
423
+ refuseValues: true,
424
+ reason: CONSENT_FROM_FILE_REASON
425
+ },
426
+ ...LIST_MEMBER_CONTACT_FIELD_IDS.map((fieldId)=>({
427
+ fieldId,
428
+ forced: {
429
+ blank: 'leave'
430
+ },
431
+ reason: CONTACT_NEVER_CLEARED_REASON
432
+ }))
433
+ ];
434
+ /*==========================================
435
+ * SUPPRESSIONS
436
+ *=========================================*/ /** Why an entry already on the list is left alone. */ export const SUPPRESSION_KEPT_REASON = 'An address already suppressed keeps its entry exactly as it is — the reason and date it was suppressed with are the record of why — and an import never removes one.';
437
+ /** A site's suppression list: every field. */ export function suppressionCatalog() {
438
+ return {
439
+ groups: [
440
+ {
441
+ id: 'suppression',
442
+ label: 'Suppression'
443
+ }
444
+ ],
445
+ standard: [
446
+ {
447
+ id: 'email',
448
+ label: 'Email',
449
+ group: 'suppression',
450
+ type: 'email',
451
+ required: true,
452
+ matchKey: true,
453
+ aliases: LIST_IMPORT_EMAIL_COLUMNS
454
+ },
455
+ {
456
+ id: 'note',
457
+ label: 'Note',
458
+ group: 'suppression',
459
+ type: 'text',
460
+ maxLength: 200,
461
+ aliases: [
462
+ 'note',
463
+ 'notes',
464
+ 'comment',
465
+ 'why'
466
+ ],
467
+ description: 'How the request reached you. Kept on a new entry only.'
468
+ },
469
+ {
470
+ id: 'reason',
471
+ label: 'Reason',
472
+ group: 'suppression',
473
+ type: 'text',
474
+ readOnly: true,
475
+ description: '`unsubscribe`, `bounce`, `complaint` or `manual`. An imported address is `manual`: a person on your team added it.'
476
+ },
477
+ {
478
+ id: 'suppressedAt',
479
+ label: 'Since',
480
+ group: 'suppression',
481
+ type: 'datetime',
482
+ readOnly: true
483
+ },
484
+ {
485
+ id: 'carriedFromHostId',
486
+ label: 'Copied from site',
487
+ group: 'suppression',
488
+ type: 'text',
489
+ readOnly: true
490
+ }
491
+ ]
492
+ };
493
+ }
494
+ /** A suppression is the address. */ export const SUPPRESSION_MATCH_KEYS = [
495
+ {
496
+ fieldId: 'email',
497
+ normalizer: 'email'
498
+ }
499
+ ];
500
+ /** The suppression import's rule: nothing already on the list is changed. */ export const SUPPRESSION_LOCKED_RULES = [
501
+ {
502
+ fieldId: 'note',
503
+ forced: {
504
+ mode: 'keepExisting',
505
+ blank: 'leave'
506
+ },
507
+ reason: SUPPRESSION_KEPT_REASON
508
+ }
509
+ ];
510
+ /** Other products' suppression-export headers. */ export const SUPPRESSION_ALIASES = [
511
+ {
512
+ source: 'Mailchimp unsubscribed export',
513
+ aliases: {
514
+ email: [
515
+ 'Email Address'
516
+ ],
517
+ note: [
518
+ 'UNSUB_REASON',
519
+ 'UNSUB_REASON_OTHER'
520
+ ]
521
+ }
522
+ },
523
+ {
524
+ source: 'Klaviyo suppression export',
525
+ aliases: {
526
+ email: [
527
+ 'Email'
528
+ ],
529
+ note: [
530
+ 'Suppression Reason'
531
+ ]
532
+ }
533
+ }
534
+ ];
535
+
536
+ //# sourceMappingURL=email-transfer-catalog.js.map