@aglyn/plugins-email 1.0.0-beta.217 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/consent-group-review.js +2 -2
- package/src/lib/components/consent-group-review.js.map +1 -1
- package/src/lib/components/dynamic-list-rule-fields.js +81 -34
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -1
- package/src/lib/components/emails-console-sections.js +3 -2
- package/src/lib/components/emails-console-sections.js.map +1 -1
- package/src/lib/components/list-import-drawer.js +2 -2
- package/src/lib/components/list-import-drawer.js.map +1 -1
- package/src/lib/components/suppression-totals.js +6 -0
- package/src/lib/components/suppression-totals.js.map +1 -1
- package/src/lib/list-import.d.ts +149 -0
- package/src/lib/list-import.js +324 -0
- package/src/lib/list-import.js.map +1 -0
- package/src/lib/model/email-record-routes.d.ts +22 -0
- package/src/lib/model/email-record-routes.js +75 -0
- package/src/lib/model/email-record-routes.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +1 -1
- package/src/lib/model/template-provenance.js +1 -1
- package/src/lib/model/template-provenance.js.map +1 -1
- package/src/lib/plugin.js +5 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server-email-drafts.js +2 -2
- package/src/lib/server-email-drafts.js.map +1 -1
- package/src/lib/server-list-gate.js +35 -10
- package/src/lib/server-list-gate.js.map +1 -1
- package/src/lib/server-list-import.js +3 -2
- package/src/lib/server-list-import.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/unsubscribe-link.js +1 -1
- package/src/lib/unsubscribe-link.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-import-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * BRINGING AN EXISTING LIST IN — the screen.\n *\n * `docs/specs/email-competitive-gaps.md` G5: a customer arriving from another\n * product has a list and, until now, no way to bring it. The server half is\n * `server-list-import.ts`; this is the surface that walks an operator through\n * it.\n *\n * ## A drawer, because an import is three decisions and not a field\n *\n * Choose the file, read what is in it, then say whether you have these\n * people's permission. Stacked above the membership table those three steps\n * would have nowhere to grow — which is exactly how the audience card's\n * create form came to offer four of a rule's nine fields — and the middle\n * step is the one that must not be cramped: it is where the screening\n * warnings and the consent readout are shown, and both exist to be READ\n * before the operator attests.\n *\n * Creating is a drawer and picking is a dialog, so this composes\n * `NavigationDrawerComponent` directly, the same way the suppressions card's\n * Add does — the console's wrapper around it lives in an application a plugin\n * library may not import.\n *\n * ## The numbers on screen are the server's, always\n *\n * Nothing here parses a file, screens it, or decides anything about consent.\n * Every count and every sentence comes back from a route that read the\n * merchant's actual contacts and both suppression lists. A screen that\n * computed its own would be a second copy of the rule on the one surface\n * whose job is to tell the operator the truth about what is about to happen.\n *\n * ## The run is a loop of bounded requests, and it says so\n *\n * A large file is not one request. The drawer calls `email/list-import-run`\n * until it answers `complete`, showing the cursor against the total as it\n * goes, so an import of forty thousand addresses is a progress bar rather\n * than a spinner that either finishes or does not. Closing the drawer stops\n * the loop and loses nothing: the job is durable, and reopening offers to\n * resume it from where it stopped.\n */\n\nimport { LIST_IMPORT_MAX_CHARACTERS, pluginDocsHelp } from '@aglyn/aglyn'\nimport { HelpTip } from '@aglyn/shared-ui-jsx'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n AlertTitle,\n Button,\n Checkbox,\n Chip,\n FormControlLabel,\n IconButton,\n LinearProgress,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nexport interface ListImportDrawerProps {\n open: boolean\n onClose: () => void\n hostId: string\n listId: string\n listName: string\n /**\n * Called whenever the run enrolls anybody.\n *\n * The audience page's subscriber total is a server aggregate taken once, so\n * an import that ran to completion behind a stale count would read as\n * having done nothing. Fired per run rather than once at the end: a large\n * import is watched, and a total that only moves at the end is a total that\n * looks stuck.\n */\n onMembershipChanged?: () => void\n}\n\n/** One address's verdict, as the preview reports it. */\ninterface AddressVerdict {\n input: string\n email: string | null\n refusal: string | null\n requiresAttestation: boolean\n summary: string\n}\n\n/** What the mechanical screening found. Signals, not verdicts. */\ninterface Screening {\n roleAccounts: number\n roleAccountSamples: string[]\n purchaseTellColumns: string[]\n declaresBasis: boolean\n}\n\n/** `email/list-import-preview`'s answer. */\ninterface ImportPreview {\n columns: string[]\n usable: number\n unusable: number\n duplicates: number\n overCeiling: boolean\n ceiling: number\n unusableSamples: string[]\n screening: Screening\n sampleSize: number\n verdicts: AddressVerdict[]\n optedIn: number\n needAttestation: number\n refused: number\n}\n\n/** The durable job, as `status` and `run` report it. */\ninterface ImportJob {\n importId: string\n status: string\n total: number\n cursor: number\n enrolled: number\n refused: number\n refusals: Record<string, number>\n attested: boolean\n}\n\n/** What a refusal reason is called on screen. */\nconst REFUSAL_LABELS: Record<string, string> = {\n declined: 'Declined marketing email',\n 'no-basis': 'No opt-in on record',\n 'suppressed-host': 'Unsubscribed from this site',\n 'suppressed-platform': 'Bounced or reported as spam',\n 'unroutable-address': 'Not a valid address',\n 'no-address': 'No address',\n}\n\nexport function ListImportDrawer(props: ListImportDrawerProps) {\n const { open, onClose, hostId, listId, listName, onMembershipChanged } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n const [text, setText] = useState('')\n const [fileName, setFileName] = useState('')\n const [preview, setPreview] = useState<ImportPreview | null>(null)\n const [attested, setAttested] = useState(false)\n const [job, setJob] = useState<ImportJob | null>(null)\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state: the loop is an `async` function that has already\n * captured its closure, so a state value it read would be the value from\n * the render that started it — the loop would go on issuing requests after\n * the operator closed the drawer, which is the one thing \"closing stops it\"\n * has to mean.\n */\n const abandoned = useRef(false)\n\n const post = useCallback(\n async (route: string, body: Record<string, unknown>) => {\n const response = await authorizedFetch(user, `/api/email/${route}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, listId, ...body }),\n })\n const payload = await response.json().catch(() => ({}))\n return { ok: response.ok, payload }\n },\n [user, hostId, listId],\n )\n\n /*\n * An unfinished import is looked for when the DRAWER OPENS and at no other\n * time — never on the page's mount. A merchant reading their audiences pays\n * nothing for a feature they did not reach for, which is the read-cost rule\n * `emails-console-read-cost.spec.tsx` meters.\n */\n useEffect(() => {\n if (!open) return undefined\n abandoned.current = false\n let active = true\n void post('list-import-status', {}).then(({ ok, payload }) => {\n if (!active || !ok) return\n const found = payload?.job as ImportJob | null\n if (found && found.status !== 'complete') setJob(found)\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const reset = useCallback(() => {\n setText('')\n setFileName('')\n setPreview(null)\n setAttested(false)\n setJob(null)\n }, [])\n\n const handleClose = useCallback(() => {\n // Stops the loop; the job survives and the next open offers to resume it.\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > LIST_IMPORT_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setFileName(file.name)\n setText(contents)\n setPreview(null)\n setAttested(false)\n },\n [enqueueSnackbar],\n )\n\n const handlePreview = useCallback(async () => {\n if (busy || !text.trim()) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-import-preview', { text })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The file could not be read.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setPreview(payload as ImportPreview)\n setAttested(false)\n } catch {\n enqueueSnackbar('The file could not be read.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, text, post, enqueueSnackbar])\n\n /**\n * Drives one job to completion, one bounded request at a time.\n *\n * The loop's exit conditions are `complete`, an error, and the operator\n * closing the drawer. It never guesses how many runs a file needs — the\n * server answers `complete` when the cursor has reached the total, which is\n * the only party that knows.\n */\n const runToCompletion = useCallback(\n async (importId: string) => {\n setBusy(true)\n try {\n for (;;) {\n if (abandoned.current) return\n const { ok, payload } = await post('list-import-run', { importId })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setJob((previous) => ({\n importId,\n status: payload?.complete ? 'complete' : 'running',\n total: Number(payload?.total ?? previous?.total ?? 0),\n cursor: Number(payload?.cursor ?? 0),\n enrolled:\n (previous?.enrolled ?? 0) + Number(payload?.ranEnrolled ?? 0),\n refused:\n (previous?.refused ?? 0) + Number(payload?.ranRefused ?? 0),\n refusals: mergeCounts(\n previous?.refusals ?? {},\n (payload?.refusals ?? {}) as Record<string, number>,\n ),\n attested: previous?.attested ?? false,\n }))\n if (Number(payload?.ranEnrolled ?? 0)) onMembershipChanged?.()\n if (payload?.complete) {\n return void enqueueSnackbar('Import finished', {\n variant: 'success',\n persist: false,\n })\n }\n }\n } finally {\n setBusy(false)\n }\n },\n [post, enqueueSnackbar, onMembershipChanged],\n )\n\n const handleStart = useCallback(async () => {\n if (busy || !preview) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-import-start', {\n text,\n // The operator's assertion, not a basis they named: the server derives\n // a pass-through from each person's own record, and this flag can only\n // ever add the attributable kind.\n attestConsent: attested,\n })\n if (!ok) {\n setBusy(false)\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not be started.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const started: ImportJob = {\n importId: String(payload?.importId ?? ''),\n status: 'running',\n total: Number(payload?.total ?? 0),\n cursor: 0,\n enrolled: 0,\n refused: 0,\n refusals: {},\n attested: payload?.attested === true,\n }\n setJob(started)\n setBusy(false)\n await runToCompletion(started.importId)\n } catch {\n setBusy(false)\n enqueueSnackbar('The import could not be started.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n }, [busy, preview, text, attested, post, enqueueSnackbar, runToCompletion])\n\n const running = !!job && job.status !== 'complete'\n const finished = !!job && job.status === 'complete'\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {`Import into \"${listName}\"`}\n </Typography>\n {/*\n The help affordance every console surface carries, pointed at the\n section that explains what happens to an imported address — which\n is the question an operator has at exactly this moment.\n */}\n <HelpTip\n {...pluginDocsHelp('emailCampaigns', { anchor: '#import-a-list' })}\n />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {job ? (\n <ImportProgressPanel\n job={job}\n busy={busy}\n onResume={() => void runToCompletion(job.importId)}\n onAnother={reset}\n />\n ) : (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'A CSV with an email column, or one address per line. Every ' +\n 'address goes through the same checks as one you type in ' +\n 'by hand: anyone who unsubscribed, bounced, or told you ' +\n 'they do not want marketing email is refused, and nothing ' +\n 'you upload can override that.'}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,.txt,text/csv,text/plain\"\n hidden\n onChange={(event) =>\n void handleFile(event.target.files?.[0])\n }\n />\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n </Stack>\n <TextField\n label=\"Or paste the list\"\n value={text}\n onChange={(event) => {\n setText(event.target.value)\n setFileName('')\n /*\n * Any edit invalidates the answer. A preview belongs to the\n * exact text it was run for, and leaving a stale one on\n * screen would show counts, and an attestation checkbox,\n * for a file the operator has since changed.\n */\n setPreview(null)\n setAttested(false)\n }}\n multiline\n minRows={4}\n maxRows={12}\n fullWidth\n helperText={\n 'One address per line, or paste the whole CSV including ' +\n 'its header row.'\n }\n />\n {preview ? (\n <ImportReviewPanel\n preview={preview}\n attested={attested}\n onAttestedChange={setAttested}\n />\n ) : null}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {preview ? (\n <Button\n variant=\"contained\"\n disabled={busy || preview.usable === 0}\n onClick={() => void handleStart()}\n >\n {attested\n ? `Import ${preview.usable} addresses`\n : `Import the ${preview.optedIn} with an opt-in`}\n </Button>\n ) : (\n <Button\n variant=\"contained\"\n disabled={busy || !text.trim()}\n onClick={() => void handlePreview()}\n >\n {busy ? 'Checking…' : 'Check this file'}\n </Button>\n )}\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nListImportDrawer.displayName = 'ListImportDrawer'\n\n/** Adds two refusal tallies together. */\nfunction mergeCounts(\n base: Record<string, number>,\n extra: Record<string, number>,\n): Record<string, number> {\n const merged = { ...base }\n for (const [key, value] of Object.entries(extra)) {\n merged[key] = (merged[key] ?? 0) + Number(value ?? 0)\n }\n return merged\n}\n\n/**\n * WHAT IS IN THE FILE, and the one question the operator has to answer.\n *\n * Three blocks in the order they should be read: what the file contains, what\n * the screening noticed, and what the consent gate said about a sample. The\n * attestation control comes last on purpose — it is the act, and an act\n * offered above its own evidence is an act taken without it.\n */\nfunction ImportReviewPanel(props: {\n preview: ImportPreview\n attested: boolean\n onAttestedChange: (value: boolean) => void\n}) {\n const { preview, attested, onAttestedChange } = props\n const { screening } = preview\n return (\n <Stack spacing={2}>\n <Alert severity=\"info\">\n <AlertTitle>{`${preview.usable} addresses in this file`}</AlertTitle>\n <Stack spacing={0.5}>\n {preview.duplicates ? (\n <Typography variant=\"body2\">\n {`${preview.duplicates} repeated addresses were collapsed — the ` +\n 'same person twice is one member.'}\n </Typography>\n ) : null}\n {preview.unusable ? (\n <Typography variant=\"body2\">\n {`${preview.unusable} lines are not email addresses and will be ` +\n `skipped${\n preview.unusableSamples.length\n ? `, starting with: ${preview.unusableSamples\n .slice(0, 3)\n .join(', ')}`\n : ''\n }.`}\n </Typography>\n ) : null}\n {preview.overCeiling ? (\n <Typography variant=\"body2\">\n {`This file names more than ${preview.ceiling} addresses. Only ` +\n 'the first are staged — split the file and import the rest ' +\n 'separately. Nothing already on the list is affected.'}\n </Typography>\n ) : null}\n </Stack>\n </Alert>\n\n {screening.purchaseTellColumns.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This file looks bought or appended'}</AlertTitle>\n {`Its columns include ${screening.purchaseTellColumns.join(', ')}. ` +\n 'Purchased, rented and appended lists are not allowed, and ' +\n 'importing one puts every site sending through this domain at ' +\n 'risk. If the column name is a coincidence, carry on.'}\n </Alert>\n ) : null}\n\n {screening.roleAccounts ? (\n <Alert severity=\"warning\">\n <AlertTitle>\n {`${screening.roleAccounts} shared mailboxes`}\n </AlertTitle>\n {`Addresses like ${screening.roleAccountSamples\n .slice(0, 3)\n .join(', ')} reach a mailbox several people read, or nobody. ` +\n 'They are a common sign of a list built by collecting addresses ' +\n 'rather than by people signing up. They will be imported if you ' +\n 'go ahead.'}\n </Alert>\n ) : null}\n\n <Alert severity={preview.needAttestation ? 'warning' : 'success'}>\n <AlertTitle>\n {preview.sampleSize < preview.usable\n ? `Checked the first ${preview.sampleSize} of ${preview.usable}`\n : 'Checked every address'}\n </AlertTitle>\n <Stack spacing={0.5}>\n <Typography variant=\"body2\">\n {`${preview.optedIn} already have an opt-in on record, ` +\n `${preview.needAttestation} have no opt-in on record, and ` +\n `${preview.refused} cannot be added at all.`}\n </Typography>\n {preview.sampleSize < preview.usable ? (\n <Typography variant=\"body2\">\n {'The rest are checked the same way as the import runs, and ' +\n 'the result is reported as it goes.'}\n </Typography>\n ) : null}\n </Stack>\n </Alert>\n\n {preview.verdicts.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Address'}</TableCell>\n <TableCell>{'What we know'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.verdicts.slice(0, 10).map((verdict, index) => (\n <TableRow key={`${verdict.input}-${index}`}>\n <TableCell>{verdict.email ?? verdict.input}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {verdict.summary}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n\n {preview.needAttestation ? (\n <FormControlLabel\n control={\n <Checkbox\n checked={attested}\n onChange={(event) => onAttestedChange(event.target.checked)}\n />\n }\n label={\n <Typography variant=\"body2\">\n {'I have these people’s permission to send them marketing ' +\n 'email, and I can produce the record of it if asked. This ' +\n 'statement is stored against my account, with today’s date, ' +\n 'on every address it admits.'}\n {screening.declaresBasis\n ? ' The opt-in source and date your file declares are kept with it.'\n : ''}\n </Typography>\n }\n />\n ) : null}\n {preview.needAttestation && !attested ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Without that statement the import adds only the addresses that ' +\n 'already have an opt-in on record. Nobody else is added, and ' +\n 'nothing is deleted.'}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportProgressPanel(props: {\n job: ImportJob\n busy: boolean\n onResume: () => void\n onAnother: () => void\n}) {\n const { job, busy, onResume, onAnother } = props\n const done = job.status === 'complete'\n const percent = job.total ? Math.min((job.cursor / job.total) * 100, 100) : 0\n const refusals = Object.entries(job.refusals).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {done\n ? `Finished. ${job.enrolled} of ${job.total} addresses were added.`\n : `${job.cursor} of ${job.total} addresses checked.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {refusals.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Not added'}</TableCell>\n <TableCell align=\"right\">{'Addresses'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {refusals.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{REFUSAL_LABELS[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {!done ? (\n <Alert severity=\"info\">\n {'Large files are added in batches, so this takes a few minutes. ' +\n 'You can close this — the import picks up where it stopped when ' +\n 'you come back.'}\n </Alert>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {done ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : (\n <Button variant=\"contained\" disabled={busy} onClick={onResume}>\n {busy ? 'Importing…' : 'Resume import'}\n </Button>\n )}\n </Stack>\n </Stack>\n )\n}\n\nexport default ListImportDrawer\n"],"names":["LIST_IMPORT_MAX_CHARACTERS","pluginDocsHelp","HelpTip","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","Alert","AlertTitle","Button","Checkbox","Chip","FormControlLabel","IconButton","LinearProgress","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useRef","useState","useUser","authorizedFetch","REFUSAL_LABELS","declined","ListImportDrawer","props","open","onClose","hostId","listId","listName","onMembershipChanged","data","user","enqueueSnackbar","text","setText","fileName","setFileName","preview","setPreview","attested","setAttested","job","setJob","busy","setBusy","abandoned","post","route","body","response","method","headers","JSON","stringify","payload","json","catch","ok","undefined","current","active","then","found","status","reset","handleClose","handleFile","file","contents","length","variant","allowDuplicate","name","handlePreview","trim","error","runToCompletion","importId","previous","complete","total","Number","cursor","enrolled","ranEnrolled","refused","ranRefused","refusals","mergeCounts","persist","handleStart","attestConsent","started","String","running","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","gutterY","spacing","ImportProgressPanel","onResume","onAnother","direction","alignItems","disabled","input","type","accept","hidden","onChange","event","target","files","size","label","value","multiline","minRows","maxRows","fullWidth","helperText","ImportReviewPanel","onAttestedChange","justifyContent","usable","optedIn","displayName","base","extra","merged","key","Object","entries","screening","severity","duplicates","unusable","unusableSamples","slice","join","overCeiling","ceiling","purchaseTellColumns","roleAccounts","roleAccountSamples","needAttestation","sampleSize","verdicts","map","verdict","index","email","summary","control","checked","declaresBasis","done","percent","Math","min","filter","count","borderRadius","align","reason"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-import-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * BRINGING AN EXISTING LIST IN — the screen.\n *\n * `docs/specs/email-competitive-gaps.md` G5: a customer arriving from another\n * product has a list and, until now, no way to bring it. The server half is\n * `server-list-import.ts`; this is the surface that walks an operator through\n * it.\n *\n * ## A drawer, because an import is three decisions and not a field\n *\n * Choose the file, read what is in it, then say whether you have these\n * people's permission. Stacked above the membership table those three steps\n * would have nowhere to grow — which is exactly how the audience card's\n * create form came to offer four of a rule's nine fields — and the middle\n * step is the one that must not be cramped: it is where the screening\n * warnings and the consent readout are shown, and both exist to be READ\n * before the operator attests.\n *\n * Creating is a drawer and picking is a dialog, so this composes\n * `NavigationDrawerComponent` directly, the same way the suppressions card's\n * Add does — the console's wrapper around it lives in an application a plugin\n * library may not import.\n *\n * ## The numbers on screen are the server's, always\n *\n * Nothing here parses a file, screens it, or decides anything about consent.\n * Every count and every sentence comes back from a route that read the\n * merchant's actual contacts and both suppression lists. A screen that\n * computed its own would be a second copy of the rule on the one surface\n * whose job is to tell the operator the truth about what is about to happen.\n *\n * ## The run is a loop of bounded requests, and it says so\n *\n * A large file is not one request. The drawer calls `email/list-import-run`\n * until it answers `complete`, showing the cursor against the total as it\n * goes, so an import of forty thousand addresses is a progress bar rather\n * than a spinner that either finishes or does not. Closing the drawer stops\n * the loop and loses nothing: the job is durable, and reopening offers to\n * resume it from where it stopped.\n */\n\nimport { CSV_UPLOAD_MAX_CHARACTERS, pluginDocsHelp } from '@aglyn/aglyn'\nimport { HelpTip } from '@aglyn/shared-ui-jsx'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n AlertTitle,\n Button,\n Checkbox,\n Chip,\n FormControlLabel,\n IconButton,\n LinearProgress,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nexport interface ListImportDrawerProps {\n open: boolean\n onClose: () => void\n hostId: string\n listId: string\n listName: string\n /**\n * Called whenever the run enrolls anybody.\n *\n * The audience page's subscriber total is a server aggregate taken once, so\n * an import that ran to completion behind a stale count would read as\n * having done nothing. Fired per run rather than once at the end: a large\n * import is watched, and a total that only moves at the end is a total that\n * looks stuck.\n */\n onMembershipChanged?: () => void\n}\n\n/** One address's verdict, as the preview reports it. */\ninterface AddressVerdict {\n input: string\n email: string | null\n refusal: string | null\n requiresAttestation: boolean\n summary: string\n}\n\n/** What the mechanical screening found. Signals, not verdicts. */\ninterface Screening {\n roleAccounts: number\n roleAccountSamples: string[]\n purchaseTellColumns: string[]\n declaresBasis: boolean\n}\n\n/** `email/list-import-preview`'s answer. */\ninterface ImportPreview {\n columns: string[]\n usable: number\n unusable: number\n duplicates: number\n overCeiling: boolean\n ceiling: number\n unusableSamples: string[]\n screening: Screening\n sampleSize: number\n verdicts: AddressVerdict[]\n optedIn: number\n needAttestation: number\n refused: number\n}\n\n/** The durable job, as `status` and `run` report it. */\ninterface ImportJob {\n importId: string\n status: string\n total: number\n cursor: number\n enrolled: number\n refused: number\n refusals: Record<string, number>\n attested: boolean\n}\n\n/** What a refusal reason is called on screen. */\nconst REFUSAL_LABELS: Record<string, string> = {\n declined: 'Declined marketing email',\n 'no-basis': 'No opt-in on record',\n 'suppressed-host': 'Unsubscribed from this site',\n 'suppressed-platform': 'Bounced or reported as spam',\n 'unroutable-address': 'Not a valid address',\n 'no-address': 'No address',\n}\n\nexport function ListImportDrawer(props: ListImportDrawerProps) {\n const { open, onClose, hostId, listId, listName, onMembershipChanged } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n const [text, setText] = useState('')\n const [fileName, setFileName] = useState('')\n const [preview, setPreview] = useState<ImportPreview | null>(null)\n const [attested, setAttested] = useState(false)\n const [job, setJob] = useState<ImportJob | null>(null)\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state: the loop is an `async` function that has already\n * captured its closure, so a state value it read would be the value from\n * the render that started it — the loop would go on issuing requests after\n * the operator closed the drawer, which is the one thing \"closing stops it\"\n * has to mean.\n */\n const abandoned = useRef(false)\n\n const post = useCallback(\n async (route: string, body: Record<string, unknown>) => {\n const response = await authorizedFetch(user, `/api/email/${route}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, listId, ...body }),\n })\n const payload = await response.json().catch(() => ({}))\n return { ok: response.ok, payload }\n },\n [user, hostId, listId],\n )\n\n /*\n * An unfinished import is looked for when the DRAWER OPENS and at no other\n * time — never on the page's mount. A merchant reading their audiences pays\n * nothing for a feature they did not reach for, which is the read-cost rule\n * `emails-console-read-cost.spec.tsx` meters.\n */\n useEffect(() => {\n if (!open) return undefined\n abandoned.current = false\n let active = true\n void post('list-import-status', {}).then(({ ok, payload }) => {\n if (!active || !ok) return\n const found = payload?.job as ImportJob | null\n if (found && found.status !== 'complete') setJob(found)\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const reset = useCallback(() => {\n setText('')\n setFileName('')\n setPreview(null)\n setAttested(false)\n setJob(null)\n }, [])\n\n const handleClose = useCallback(() => {\n // Stops the loop; the job survives and the next open offers to resume it.\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > CSV_UPLOAD_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setFileName(file.name)\n setText(contents)\n setPreview(null)\n setAttested(false)\n },\n [enqueueSnackbar],\n )\n\n const handlePreview = useCallback(async () => {\n if (busy || !text.trim()) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-import-preview', { text })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The file could not be read.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setPreview(payload as ImportPreview)\n setAttested(false)\n } catch {\n enqueueSnackbar('The file could not be read.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, text, post, enqueueSnackbar])\n\n /**\n * Drives one job to completion, one bounded request at a time.\n *\n * The loop's exit conditions are `complete`, an error, and the operator\n * closing the drawer. It never guesses how many runs a file needs — the\n * server answers `complete` when the cursor has reached the total, which is\n * the only party that knows.\n */\n const runToCompletion = useCallback(\n async (importId: string) => {\n setBusy(true)\n try {\n for (;;) {\n if (abandoned.current) return\n const { ok, payload } = await post('list-import-run', { importId })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setJob((previous) => ({\n importId,\n status: payload?.complete ? 'complete' : 'running',\n total: Number(payload?.total ?? previous?.total ?? 0),\n cursor: Number(payload?.cursor ?? 0),\n enrolled:\n (previous?.enrolled ?? 0) + Number(payload?.ranEnrolled ?? 0),\n refused:\n (previous?.refused ?? 0) + Number(payload?.ranRefused ?? 0),\n refusals: mergeCounts(\n previous?.refusals ?? {},\n (payload?.refusals ?? {}) as Record<string, number>,\n ),\n attested: previous?.attested ?? false,\n }))\n if (Number(payload?.ranEnrolled ?? 0)) onMembershipChanged?.()\n if (payload?.complete) {\n return void enqueueSnackbar('Import finished', {\n variant: 'success',\n persist: false,\n })\n }\n }\n } finally {\n setBusy(false)\n }\n },\n [post, enqueueSnackbar, onMembershipChanged],\n )\n\n const handleStart = useCallback(async () => {\n if (busy || !preview) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-import-start', {\n text,\n // The operator's assertion, not a basis they named: the server derives\n // a pass-through from each person's own record, and this flag can only\n // ever add the attributable kind.\n attestConsent: attested,\n })\n if (!ok) {\n setBusy(false)\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not be started.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const started: ImportJob = {\n importId: String(payload?.importId ?? ''),\n status: 'running',\n total: Number(payload?.total ?? 0),\n cursor: 0,\n enrolled: 0,\n refused: 0,\n refusals: {},\n attested: payload?.attested === true,\n }\n setJob(started)\n setBusy(false)\n await runToCompletion(started.importId)\n } catch {\n setBusy(false)\n enqueueSnackbar('The import could not be started.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n }, [busy, preview, text, attested, post, enqueueSnackbar, runToCompletion])\n\n const running = !!job && job.status !== 'complete'\n const finished = !!job && job.status === 'complete'\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {`Import into \"${listName}\"`}\n </Typography>\n {/*\n The help affordance every console surface carries, pointed at the\n section that explains what happens to an imported address — which\n is the question an operator has at exactly this moment.\n */}\n <HelpTip\n {...pluginDocsHelp('emailCampaigns', { anchor: '#import-a-list' })}\n />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {job ? (\n <ImportProgressPanel\n job={job}\n busy={busy}\n onResume={() => void runToCompletion(job.importId)}\n onAnother={reset}\n />\n ) : (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'A CSV with an email column, or one address per line. Every ' +\n 'address goes through the same checks as one you type in ' +\n 'by hand: anyone who unsubscribed, bounced, or told you ' +\n 'they do not want marketing email is refused, and nothing ' +\n 'you upload can override that.'}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,.txt,text/csv,text/plain\"\n hidden\n onChange={(event) =>\n void handleFile(event.target.files?.[0])\n }\n />\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n </Stack>\n <TextField\n label=\"Or paste the list\"\n value={text}\n onChange={(event) => {\n setText(event.target.value)\n setFileName('')\n /*\n * Any edit invalidates the answer. A preview belongs to the\n * exact text it was run for, and leaving a stale one on\n * screen would show counts, and an attestation checkbox,\n * for a file the operator has since changed.\n */\n setPreview(null)\n setAttested(false)\n }}\n multiline\n minRows={4}\n maxRows={12}\n fullWidth\n helperText={\n 'One address per line, or paste the whole CSV including ' +\n 'its header row.'\n }\n />\n {preview ? (\n <ImportReviewPanel\n preview={preview}\n attested={attested}\n onAttestedChange={setAttested}\n />\n ) : null}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {preview ? (\n <Button\n variant=\"contained\"\n disabled={busy || preview.usable === 0}\n onClick={() => void handleStart()}\n >\n {attested\n ? `Import ${preview.usable} addresses`\n : `Import the ${preview.optedIn} with an opt-in`}\n </Button>\n ) : (\n <Button\n variant=\"contained\"\n disabled={busy || !text.trim()}\n onClick={() => void handlePreview()}\n >\n {busy ? 'Checking…' : 'Check this file'}\n </Button>\n )}\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nListImportDrawer.displayName = 'ListImportDrawer'\n\n/** Adds two refusal tallies together. */\nfunction mergeCounts(\n base: Record<string, number>,\n extra: Record<string, number>,\n): Record<string, number> {\n const merged = { ...base }\n for (const [key, value] of Object.entries(extra)) {\n merged[key] = (merged[key] ?? 0) + Number(value ?? 0)\n }\n return merged\n}\n\n/**\n * WHAT IS IN THE FILE, and the one question the operator has to answer.\n *\n * Three blocks in the order they should be read: what the file contains, what\n * the screening noticed, and what the consent gate said about a sample. The\n * attestation control comes last on purpose — it is the act, and an act\n * offered above its own evidence is an act taken without it.\n */\nfunction ImportReviewPanel(props: {\n preview: ImportPreview\n attested: boolean\n onAttestedChange: (value: boolean) => void\n}) {\n const { preview, attested, onAttestedChange } = props\n const { screening } = preview\n return (\n <Stack spacing={2}>\n <Alert severity=\"info\">\n <AlertTitle>{`${preview.usable} addresses in this file`}</AlertTitle>\n <Stack spacing={0.5}>\n {preview.duplicates ? (\n <Typography variant=\"body2\">\n {`${preview.duplicates} repeated addresses were collapsed — the ` +\n 'same person twice is one member.'}\n </Typography>\n ) : null}\n {preview.unusable ? (\n <Typography variant=\"body2\">\n {`${preview.unusable} lines are not email addresses and will be ` +\n `skipped${\n preview.unusableSamples.length\n ? `, starting with: ${preview.unusableSamples\n .slice(0, 3)\n .join(', ')}`\n : ''\n }.`}\n </Typography>\n ) : null}\n {preview.overCeiling ? (\n <Typography variant=\"body2\">\n {`This file names more than ${preview.ceiling} addresses. Only ` +\n 'the first are staged — split the file and import the rest ' +\n 'separately. Nothing already on the list is affected.'}\n </Typography>\n ) : null}\n </Stack>\n </Alert>\n\n {screening.purchaseTellColumns.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This file looks bought or appended'}</AlertTitle>\n {`Its columns include ${screening.purchaseTellColumns.join(', ')}. ` +\n 'Purchased, rented and appended lists are not allowed, and ' +\n 'importing one puts every site sending through this domain at ' +\n 'risk. If the column name is a coincidence, carry on.'}\n </Alert>\n ) : null}\n\n {screening.roleAccounts ? (\n <Alert severity=\"warning\">\n <AlertTitle>\n {`${screening.roleAccounts} shared mailboxes`}\n </AlertTitle>\n {`Addresses like ${screening.roleAccountSamples\n .slice(0, 3)\n .join(', ')} reach a mailbox several people read, or nobody. ` +\n 'They are a common sign of a list built by collecting addresses ' +\n 'rather than by people signing up. They will be imported if you ' +\n 'go ahead.'}\n </Alert>\n ) : null}\n\n <Alert severity={preview.needAttestation ? 'warning' : 'success'}>\n <AlertTitle>\n {preview.sampleSize < preview.usable\n ? `Checked the first ${preview.sampleSize} of ${preview.usable}`\n : 'Checked every address'}\n </AlertTitle>\n <Stack spacing={0.5}>\n <Typography variant=\"body2\">\n {`${preview.optedIn} already have an opt-in on record, ` +\n `${preview.needAttestation} have no opt-in on record, and ` +\n `${preview.refused} cannot be added at all.`}\n </Typography>\n {preview.sampleSize < preview.usable ? (\n <Typography variant=\"body2\">\n {'The rest are checked the same way as the import runs, and ' +\n 'the result is reported as it goes.'}\n </Typography>\n ) : null}\n </Stack>\n </Alert>\n\n {preview.verdicts.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Address'}</TableCell>\n <TableCell>{'What we know'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.verdicts.slice(0, 10).map((verdict, index) => (\n <TableRow key={`${verdict.input}-${index}`}>\n <TableCell>{verdict.email ?? verdict.input}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {verdict.summary}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n\n {preview.needAttestation ? (\n <FormControlLabel\n control={\n <Checkbox\n checked={attested}\n onChange={(event) => onAttestedChange(event.target.checked)}\n />\n }\n label={\n <Typography variant=\"body2\">\n {'I have these people’s permission to send them marketing ' +\n 'email, and I can produce the record of it if asked. This ' +\n 'statement is stored against my account, with today’s date, ' +\n 'on every address it admits.'}\n {screening.declaresBasis\n ? ' The opt-in source and date your file declares are kept with it.'\n : ''}\n </Typography>\n }\n />\n ) : null}\n {preview.needAttestation && !attested ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Without that statement the import adds only the addresses that ' +\n 'already have an opt-in on record. Nobody else is added, and ' +\n 'nothing is deleted.'}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportProgressPanel(props: {\n job: ImportJob\n busy: boolean\n onResume: () => void\n onAnother: () => void\n}) {\n const { job, busy, onResume, onAnother } = props\n const done = job.status === 'complete'\n const percent = job.total ? Math.min((job.cursor / job.total) * 100, 100) : 0\n const refusals = Object.entries(job.refusals).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {done\n ? `Finished. ${job.enrolled} of ${job.total} addresses were added.`\n : `${job.cursor} of ${job.total} addresses checked.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {refusals.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Not added'}</TableCell>\n <TableCell align=\"right\">{'Addresses'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {refusals.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{REFUSAL_LABELS[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {!done ? (\n <Alert severity=\"info\">\n {'Large files are added in batches, so this takes a few minutes. ' +\n 'You can close this — the import picks up where it stopped when ' +\n 'you come back.'}\n </Alert>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {done ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : (\n <Button variant=\"contained\" disabled={busy} onClick={onResume}>\n {busy ? 'Importing…' : 'Resume import'}\n </Button>\n )}\n </Stack>\n </Stack>\n )\n}\n\nexport default ListImportDrawer\n"],"names":["CSV_UPLOAD_MAX_CHARACTERS","pluginDocsHelp","HelpTip","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","Alert","AlertTitle","Button","Checkbox","Chip","FormControlLabel","IconButton","LinearProgress","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useRef","useState","useUser","authorizedFetch","REFUSAL_LABELS","declined","ListImportDrawer","props","open","onClose","hostId","listId","listName","onMembershipChanged","data","user","enqueueSnackbar","text","setText","fileName","setFileName","preview","setPreview","attested","setAttested","job","setJob","busy","setBusy","abandoned","post","route","body","response","method","headers","JSON","stringify","payload","json","catch","ok","undefined","current","active","then","found","status","reset","handleClose","handleFile","file","contents","length","variant","allowDuplicate","name","handlePreview","trim","error","runToCompletion","importId","previous","complete","total","Number","cursor","enrolled","ranEnrolled","refused","ranRefused","refusals","mergeCounts","persist","handleStart","attestConsent","started","String","running","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","gutterY","spacing","ImportProgressPanel","onResume","onAnother","direction","alignItems","disabled","input","type","accept","hidden","onChange","event","target","files","size","label","value","multiline","minRows","maxRows","fullWidth","helperText","ImportReviewPanel","onAttestedChange","justifyContent","usable","optedIn","displayName","base","extra","merged","key","Object","entries","screening","severity","duplicates","unusable","unusableSamples","slice","join","overCeiling","ceiling","purchaseTellColumns","roleAccounts","roleAccountSamples","needAttestation","sampleSize","verdicts","map","verdict","index","email","summary","control","checked","declaresBasis","done","percent","Math","min","filter","count","borderRadius","align","reason"],"mappings":"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"}
|
|
@@ -46,6 +46,12 @@
|
|
|
46
46
|
*/ manual: {
|
|
47
47
|
label: 'Added by hand',
|
|
48
48
|
color: 'default'
|
|
49
|
+
},
|
|
50
|
+
// Aglyn's own sites only: the account holding the address is locked, and
|
|
51
|
+
// lifting the lock removes the row (AGL-3420).
|
|
52
|
+
account_lock: {
|
|
53
|
+
label: 'Account locked',
|
|
54
|
+
color: 'error'
|
|
49
55
|
}
|
|
50
56
|
};
|
|
51
57
|
/** One stored reason, as {@link SUPPRESSION_REASONS} describes it. */ export const describeSuppressionReason = (reason)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/suppression-totals.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n collection,\n count,\n getAggregateFromServer,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore'\n\n/**\n * What a reason means to a merchant, and how much it should worry them.\n *\n * An ABSENT reason reads as \"Unsubscribed\", and that is a compatibility rule\n * rather than a guess: until AGL-2408 the unsubscribe handler wrote\n * `{ email, createdAt }` and nothing else, while the Resend webhook has\n * stamped `'bounce'`/`'complaint'` since AGL-1918 — so an entry with no reason\n * can only have come from somebody clicking the link. New unsubscribes write\n * the reason explicitly, so this fallback covers history and nothing else.\n */\nexport const SUPPRESSION_REASONS: Record<\n string,\n { label: string; color: 'default' | 'warning' | 'error' }\n> = {\n unsubscribe: { label: 'Unsubscribed', color: 'default' },\n bounce: { label: 'Bounced', color: 'warning' },\n complaint: { label: 'Marked as spam', color: 'error' },\n /*\n * Recorded by a person, through the Add control.\n *\n * Its OWN value rather than a reuse of `unsubscribe`: an opt-out arriving\n * by reply, phone or in person is not somebody clicking a link, and the\n * difference is exactly what a merchant asked to prove the request was\n * honored has to be able to show.\n */\n manual: { label: 'Added by hand', color: 'default' },\n}\n\n/** One stored reason, as {@link SUPPRESSION_REASONS} describes it. */\nexport const describeSuppressionReason = (reason: unknown) =>\n SUPPRESSION_REASONS[String(reason ?? 'unsubscribe')] ?? {\n label: String(reason),\n color: 'default' as const,\n }\n\n/** One site's suppression list, broken down by why each address is on it. */\nexport interface SuppressionTotals {\n unsubscribe: number\n bounce: number\n complaint: number\n manual: number\n}\n\n/**\n * THE BREAKDOWN OF ONE SITE'S SUPPRESSION LIST, as server aggregates.\n *\n * One reader for both the site's own suppression card and the organization's\n * summary of every site's, so the two can never count differently.\n *\n * Four reads, not one per reason. `where('reason','==','unsubscribe')` cannot\n * be asked, because an entry written before AGL-2408 carries no `reason` at\n * all and an equality filter excludes it — the field-presence trap the list's\n * ordering also avoids. Unsubscribes are therefore the REMAINDER: the total\n * minus every reason that is always written explicitly, which is exactly the\n * compatibility rule the card's `describeReason` applies row by row. A reason\n * counted explicitly and left out of the subtraction would be reported as\n * somebody who clicked unsubscribe.\n *\n * Rejects when any read fails; a caller holds its figure at \"unknown\" rather\n * than zeroing it, because \"Bounced: 0\" is a confident wrong number in the\n * reassuring direction.\n */\nexport async function readSuppressionTotals(\n firestore: Firestore,\n hostId: string,\n): Promise<SuppressionTotals> {\n const suppressionsRef = collection(firestore, 'hosts', hostId, 'suppressions')\n const [all, bounced, complained, added] = await Promise.all([\n getAggregateFromServer(suppressionsRef, { total: count() }),\n getAggregateFromServer(\n query(suppressionsRef, where('reason', '==', 'bounce')),\n { total: count() },\n ),\n getAggregateFromServer(\n query(suppressionsRef, where('reason', '==', 'complaint')),\n { total: count() },\n ),\n getAggregateFromServer(\n query(suppressionsRef, where('reason', '==', 'manual')),\n { total: count() },\n ),\n ])\n const total = Number(all.data().total ?? 0)\n const bounce = Number(bounced.data().total ?? 0)\n const complaint = Number(complained.data().total ?? 0)\n const manual = Number(added.data().total ?? 0)\n return {\n unsubscribe: Math.max(0, total - bounce - complaint - manual),\n bounce,\n complaint,\n manual,\n }\n}\n"],"names":["collection","count","getAggregateFromServer","query","where","SUPPRESSION_REASONS","unsubscribe","label","color","bounce","complaint","manual","describeSuppressionReason","reason","String","readSuppressionTotals","firestore","hostId","all","bounced","complained","added","suppressionsRef","Promise","total","Number","data","Math","max"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,UAAU,EACVC,KAAK,EACLC,sBAAsB,EACtBC,KAAK,EACLC,KAAK,QAEA,qBAAoB;AAE3B;;;;;;;;;CASC,GACD,OAAO,MAAMC,sBAGT;IACFC,aAAa;QAAEC,OAAO;QAAgBC,OAAO;IAAU;IACvDC,QAAQ;QAAEF,OAAO;QAAWC,OAAO;IAAU;IAC7CE,WAAW;QAAEH,OAAO;QAAkBC,OAAO;IAAQ;IACrD;;;;;;;GAOC,GACDG,QAAQ;QAAEJ,OAAO;QAAiBC,OAAO;IAAU;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/suppression-totals.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n collection,\n count,\n getAggregateFromServer,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore'\n\n/**\n * What a reason means to a merchant, and how much it should worry them.\n *\n * An ABSENT reason reads as \"Unsubscribed\", and that is a compatibility rule\n * rather than a guess: until AGL-2408 the unsubscribe handler wrote\n * `{ email, createdAt }` and nothing else, while the Resend webhook has\n * stamped `'bounce'`/`'complaint'` since AGL-1918 — so an entry with no reason\n * can only have come from somebody clicking the link. New unsubscribes write\n * the reason explicitly, so this fallback covers history and nothing else.\n */\nexport const SUPPRESSION_REASONS: Record<\n string,\n { label: string; color: 'default' | 'warning' | 'error' }\n> = {\n unsubscribe: { label: 'Unsubscribed', color: 'default' },\n bounce: { label: 'Bounced', color: 'warning' },\n complaint: { label: 'Marked as spam', color: 'error' },\n /*\n * Recorded by a person, through the Add control.\n *\n * Its OWN value rather than a reuse of `unsubscribe`: an opt-out arriving\n * by reply, phone or in person is not somebody clicking a link, and the\n * difference is exactly what a merchant asked to prove the request was\n * honored has to be able to show.\n */\n manual: { label: 'Added by hand', color: 'default' },\n // Aglyn's own sites only: the account holding the address is locked, and\n // lifting the lock removes the row (AGL-3420).\n account_lock: { label: 'Account locked', color: 'error' },\n}\n\n/** One stored reason, as {@link SUPPRESSION_REASONS} describes it. */\nexport const describeSuppressionReason = (reason: unknown) =>\n SUPPRESSION_REASONS[String(reason ?? 'unsubscribe')] ?? {\n label: String(reason),\n color: 'default' as const,\n }\n\n/** One site's suppression list, broken down by why each address is on it. */\nexport interface SuppressionTotals {\n unsubscribe: number\n bounce: number\n complaint: number\n manual: number\n}\n\n/**\n * THE BREAKDOWN OF ONE SITE'S SUPPRESSION LIST, as server aggregates.\n *\n * One reader for both the site's own suppression card and the organization's\n * summary of every site's, so the two can never count differently.\n *\n * Four reads, not one per reason. `where('reason','==','unsubscribe')` cannot\n * be asked, because an entry written before AGL-2408 carries no `reason` at\n * all and an equality filter excludes it — the field-presence trap the list's\n * ordering also avoids. Unsubscribes are therefore the REMAINDER: the total\n * minus every reason that is always written explicitly, which is exactly the\n * compatibility rule the card's `describeReason` applies row by row. A reason\n * counted explicitly and left out of the subtraction would be reported as\n * somebody who clicked unsubscribe.\n *\n * Rejects when any read fails; a caller holds its figure at \"unknown\" rather\n * than zeroing it, because \"Bounced: 0\" is a confident wrong number in the\n * reassuring direction.\n */\nexport async function readSuppressionTotals(\n firestore: Firestore,\n hostId: string,\n): Promise<SuppressionTotals> {\n const suppressionsRef = collection(firestore, 'hosts', hostId, 'suppressions')\n const [all, bounced, complained, added] = await Promise.all([\n getAggregateFromServer(suppressionsRef, { total: count() }),\n getAggregateFromServer(\n query(suppressionsRef, where('reason', '==', 'bounce')),\n { total: count() },\n ),\n getAggregateFromServer(\n query(suppressionsRef, where('reason', '==', 'complaint')),\n { total: count() },\n ),\n getAggregateFromServer(\n query(suppressionsRef, where('reason', '==', 'manual')),\n { total: count() },\n ),\n ])\n const total = Number(all.data().total ?? 0)\n const bounce = Number(bounced.data().total ?? 0)\n const complaint = Number(complained.data().total ?? 0)\n const manual = Number(added.data().total ?? 0)\n return {\n unsubscribe: Math.max(0, total - bounce - complaint - manual),\n bounce,\n complaint,\n manual,\n }\n}\n"],"names":["collection","count","getAggregateFromServer","query","where","SUPPRESSION_REASONS","unsubscribe","label","color","bounce","complaint","manual","account_lock","describeSuppressionReason","reason","String","readSuppressionTotals","firestore","hostId","all","bounced","complained","added","suppressionsRef","Promise","total","Number","data","Math","max"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,UAAU,EACVC,KAAK,EACLC,sBAAsB,EACtBC,KAAK,EACLC,KAAK,QAEA,qBAAoB;AAE3B;;;;;;;;;CASC,GACD,OAAO,MAAMC,sBAGT;IACFC,aAAa;QAAEC,OAAO;QAAgBC,OAAO;IAAU;IACvDC,QAAQ;QAAEF,OAAO;QAAWC,OAAO;IAAU;IAC7CE,WAAW;QAAEH,OAAO;QAAkBC,OAAO;IAAQ;IACrD;;;;;;;GAOC,GACDG,QAAQ;QAAEJ,OAAO;QAAiBC,OAAO;IAAU;IACnD,yEAAyE;IACzE,+CAA+C;IAC/CI,cAAc;QAAEL,OAAO;QAAkBC,OAAO;IAAQ;AAC1D,EAAC;AAED,oEAAoE,GACpE,OAAO,MAAMK,4BAA4B,CAACC;QACxCT;YAAAA,8BAAAA,mBAAmB,CAACU,OAAOD,iBAAAA,SAAU,eAAe,YAApDT,8BAAwD;QACtDE,OAAOQ,OAAOD;QACdN,OAAO;IACT;EAAC;AAUH;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,eAAeQ,sBACpBC,SAAoB,EACpBC,MAAc;QAkBOC,iBACCC,qBACGC,wBACHC;IAnBtB,MAAMC,kBAAkBvB,WAAWiB,WAAW,SAASC,QAAQ;IAC/D,MAAM,CAACC,KAAKC,SAASC,YAAYC,MAAM,GAAG,MAAME,QAAQL,GAAG,CAAC;QAC1DjB,uBAAuBqB,iBAAiB;YAAEE,OAAOxB;QAAQ;QACzDC,uBACEC,MAAMoB,iBAAiBnB,MAAM,UAAU,MAAM,YAC7C;YAAEqB,OAAOxB;QAAQ;QAEnBC,uBACEC,MAAMoB,iBAAiBnB,MAAM,UAAU,MAAM,eAC7C;YAAEqB,OAAOxB;QAAQ;QAEnBC,uBACEC,MAAMoB,iBAAiBnB,MAAM,UAAU,MAAM,YAC7C;YAAEqB,OAAOxB;QAAQ;KAEpB;IACD,MAAMwB,QAAQC,QAAOP,kBAAAA,IAAIQ,IAAI,GAAGF,KAAK,YAAhBN,kBAAoB;IACzC,MAAMV,SAASiB,QAAON,sBAAAA,QAAQO,IAAI,GAAGF,KAAK,YAApBL,sBAAwB;IAC9C,MAAMV,YAAYgB,QAAOL,yBAAAA,WAAWM,IAAI,GAAGF,KAAK,YAAvBJ,yBAA2B;IACpD,MAAMV,SAASe,QAAOJ,oBAAAA,MAAMK,IAAI,GAAGF,KAAK,YAAlBH,oBAAsB;IAC5C,OAAO;QACLhB,aAAasB,KAAKC,GAAG,CAAC,GAAGJ,QAAQhB,SAASC,YAAYC;QACtDF;QACAC;QACAC;IACF;AACF"}
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
* The most addresses one uploaded file may name.
|
|
19
|
+
*
|
|
20
|
+
* A bound on the WORK an import represents, not a capacity limit: it refuses
|
|
21
|
+
* a FILE, before anything is written, and it can never drop a person already
|
|
22
|
+
* on a list or trim one to make room. A merchant with more than this splits
|
|
23
|
+
* the file, which is a nuisance; a merchant whose audience got silently
|
|
24
|
+
* truncated has an audience they cannot reason about.
|
|
25
|
+
*
|
|
26
|
+
* 50,000 is Klaviyo's shape rather than a number of our own — its only
|
|
27
|
+
* published hard import limit is a 50 MB CSV, which is the same statement in
|
|
28
|
+
* bytes. Above this the parse itself is the cost, and it happens in one
|
|
29
|
+
* request.
|
|
30
|
+
*/
|
|
31
|
+
export declare const LIST_IMPORT_MAX_ADDRESSES = 50000;
|
|
32
|
+
/**
|
|
33
|
+
* Local parts that make an address a ROLE account.
|
|
34
|
+
*
|
|
35
|
+
* A mailbox several people read, or none. M3AAWG's Vetting BCP: their
|
|
36
|
+
* appearance on a customer list "may be indicative of poor acquisition
|
|
37
|
+
* practices" — a list built by scraping a website collects these, a list
|
|
38
|
+
* built from signups does not.
|
|
39
|
+
*
|
|
40
|
+
* Reported, never dropped. A merchant importing their own supplier contacts
|
|
41
|
+
* legitimately has `orders@`, and an importer that quietly removed addresses
|
|
42
|
+
* would be telling the operator a different number went on the list than did.
|
|
43
|
+
*/
|
|
44
|
+
export declare const ROLE_ACCOUNT_LOCAL_PARTS: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* Column names that say where the file came from, when the answer is "not
|
|
47
|
+
* from people who asked".
|
|
48
|
+
*
|
|
49
|
+
* M3AAWG names `jigsaw` and `append` specifically. The rest are the same tell
|
|
50
|
+
* in the vocabulary the data brokers actually use, and they are matched as
|
|
51
|
+
* substrings of a normalized header so `Appended Email` and `append_date`
|
|
52
|
+
* both hit.
|
|
53
|
+
*
|
|
54
|
+
* Every vendor surveyed prohibits purchased lists outright, Apple bans
|
|
55
|
+
* purchased, rented and appended lists, and M3AAWG calls appending "a direct
|
|
56
|
+
* violation of core M3AAWG values". This is not a policy we invented and the
|
|
57
|
+
* screening exists so that a merchant cannot attest their way past it without
|
|
58
|
+
* having been shown it.
|
|
59
|
+
*/
|
|
60
|
+
export declare const PURCHASE_TELL_COLUMNS: readonly string[];
|
|
61
|
+
/** What the merchant's file says about one person. */
|
|
62
|
+
export interface ListImportRow {
|
|
63
|
+
/** The source line, verbatim, so a bad one can be pointed at on screen. */
|
|
64
|
+
input: string;
|
|
65
|
+
/** The normalized address, or `null` when the line does not carry one. */
|
|
66
|
+
email: string | null;
|
|
67
|
+
/** A display name from the file, or `''`. */
|
|
68
|
+
name: string;
|
|
69
|
+
/**
|
|
70
|
+
* Where the file says this person opted in, or `''`.
|
|
71
|
+
*
|
|
72
|
+
* Evidence for the operator's assertion and never a basis of its own — see
|
|
73
|
+
* the module note.
|
|
74
|
+
*/
|
|
75
|
+
declaredSource: string;
|
|
76
|
+
/** When the file says they opted in, or `''`, kept as written. */
|
|
77
|
+
declaredAt: string;
|
|
78
|
+
}
|
|
79
|
+
/** What one file turned out to be. */
|
|
80
|
+
export interface ParsedListImport {
|
|
81
|
+
/** The header row's column names, or `[]` for a bare list of addresses. */
|
|
82
|
+
columns: string[];
|
|
83
|
+
/**
|
|
84
|
+
* One row per usable, first-seen address, plus one per unusable line.
|
|
85
|
+
*
|
|
86
|
+
* A repeat of an address already seen is collapsed and counted in
|
|
87
|
+
* {@link duplicates} rather than carried: the membership is keyed by the
|
|
88
|
+
* normalized address, so two lines would be one row and reporting them as
|
|
89
|
+
* two would tell the operator they are attesting for more people than they
|
|
90
|
+
* are. That is exactly the rule `resolveAddresses` applies to a paste.
|
|
91
|
+
*/
|
|
92
|
+
rows: ListImportRow[];
|
|
93
|
+
/** Rows carrying a usable address. */
|
|
94
|
+
usable: number;
|
|
95
|
+
/** Lines that are not addresses. Reported, never dropped. */
|
|
96
|
+
unusable: number;
|
|
97
|
+
/** Repeat appearances of an address already counted. */
|
|
98
|
+
duplicates: number;
|
|
99
|
+
/** True when the file names more than {@link LIST_IMPORT_MAX_ADDRESSES}. */
|
|
100
|
+
overCeiling: boolean;
|
|
101
|
+
}
|
|
102
|
+
/** What the mechanical screening found. Signals, not verdicts. */
|
|
103
|
+
export interface ListImportScreening {
|
|
104
|
+
/** Addresses at a shared or unattended mailbox. */
|
|
105
|
+
roleAccounts: string[];
|
|
106
|
+
/** Column names in the file that read as purchase or append tells. */
|
|
107
|
+
purchaseTellColumns: string[];
|
|
108
|
+
/** True when the file declares an opt-in source or date per row. */
|
|
109
|
+
declaresBasis: boolean;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reads an uploaded or pasted file into rows.
|
|
113
|
+
*
|
|
114
|
+
* Handles the two shapes a merchant's list actually arrives in: a CSV with a
|
|
115
|
+
* header, and a bare column of addresses with no header at all. Both go
|
|
116
|
+
* through one parser — `parseCsv` — because a newline-separated list of
|
|
117
|
+
* addresses IS a single-column CSV, and a second code path for it would be a
|
|
118
|
+
* second place for the quoting rules to differ.
|
|
119
|
+
*
|
|
120
|
+
* ## Every line comes back, including the bad ones
|
|
121
|
+
*
|
|
122
|
+
* A file that silently discarded its malformed lines would tell an operator
|
|
123
|
+
* that 100 addresses went on the list when 94 did, and the six they never
|
|
124
|
+
* hear about are the six they typed wrong. This is the rule `resolveAddresses`
|
|
125
|
+
* already applies to a paste, restated for a file.
|
|
126
|
+
*/
|
|
127
|
+
export declare function parseListImport(text: string): ParsedListImport;
|
|
128
|
+
/**
|
|
129
|
+
* The mechanical checks, over a parsed file.
|
|
130
|
+
*
|
|
131
|
+
* Cheap by construction: two substring passes and one set membership per
|
|
132
|
+
* address. It is deliberately not a deliverability service — Omnisend's own
|
|
133
|
+
* caveat on the one that exists is that "list cleaning confirms
|
|
134
|
+
* deliverability only and does not establish recipient consent", and consent
|
|
135
|
+
* is the only question this import actually turns on.
|
|
136
|
+
*/
|
|
137
|
+
export declare function screenListImport(parsed: Pick<ParsedListImport, 'columns' | 'rows'>): ListImportScreening;
|
|
138
|
+
/**
|
|
139
|
+
* The sentence recorded as the REASON behind an imported person's basis.
|
|
140
|
+
*
|
|
141
|
+
* One line, built from what the file actually declared, so a compliance
|
|
142
|
+
* question about one address gets an answer about that address rather than
|
|
143
|
+
* "somebody ticked a box on an import once". Empty declarations produce the
|
|
144
|
+
* plain sentence rather than a sentence with holes in it.
|
|
145
|
+
*/
|
|
146
|
+
export declare function importedBasisReason(row: {
|
|
147
|
+
declaredSource: string;
|
|
148
|
+
declaredAt: string;
|
|
149
|
+
}): string;
|