CAN-SPAM §5(a)(4) and TCPA §227 prohibit re-adding users to marketing lists after they have opted out, unless they give fresh affirmative consent. A user import script that upserts with marketingOptOut: false for every row silently re-subscribes everyone who previously opted out — this is one of the most common AI-built app violations because the import code is often generated without awareness of the suppression table. GDPR Art. 7(3) is equally explicit: withdrawal of consent must not be overridden by subsequent data operations.
Low because the violation requires a specific write operation (import, sync, or reactivation) to trigger, but when it does, it re-subscribes potentially thousands of opted-out users in a single batch operation.
Use upsert update blocks that never touch the marketingOptOut field, and cross-reference your suppression list before any bulk send.
// scripts/importUsers.ts — preserve opt-out on upsert
for (const row of csvRows) {
await db.subscriber.upsert({
where: { email: row.email },
create: { email: row.email, name: row.name, marketingOptOut: false },
update: {
name: row.name,
// Never include marketingOptOut here — preserve whatever is in the DB
},
})
}
// Before any bulk campaign: filter against suppression
const suppressed = new Set(
(await db.subscriber.findMany({ where: { marketingOptOut: true }, select: { email: true } }))
.map(s => s.email)
)
const recipients = allRecipients.filter(r => !suppressed.has(r.email))
Apply the same logic to account reactivation flows: never reset notification preferences to defaults.
ID: email-sms-compliance.unsubscribe.no-re-subscribe
Severity: low
What to look for: Enumerate every relevant item. CAN-SPAM and TCPA prohibit re-adding users to marketing lists after they have opted out, unless the user gives fresh affirmative consent. Common AI-built app violations: a bulk import script that re-imports opted-out emails from a CRM export without checking suppression status, a sync job that overwrites the opt-out flag when syncing users from another system, or an account reactivation flow that silently re-subscribes the user to all marketing. Check any user import or sync scripts. Check whether the account reactivation or resubscription flow includes an explicit new consent step. Check whether the suppression list is checked before adding emails to any send list.
Pass criteria: At least 1 of the following conditions is met. Opt-out status is preserved across all user data operations — imports, syncs, and reactivations cannot override an existing opt-out without a new explicit consent action from the user. Any list import script consults the suppression list before adding subscribers.
Fail criteria: User import script overwrites marketingOptOut to false for all imported users. Account reactivation resets opt-out status. No suppression check in the import or sync path.
Skip (N/A) when: The application has no marketing email or SMS, or has no import/sync functionality.
Detail on fail: Example: "User import script at scripts/importUsers.ts upserts users with marketingOptOut: false for all rows, overwriting existing opt-out status." or "Account reactivation handler resets all notification preferences to defaults, including re-opting users into marketing emails.".
Remediation: Guard opt-out status in all write paths:
// scripts/importUsers.ts — always preserve existing opt-out on upsert
for (const row of csvRows) {
await db.subscriber.upsert({
where: { email: row.email },
create: {
email: row.email,
name: row.name,
marketingOptOut: false, // new users start opted in (if they gave consent)
},
update: {
name: row.name,
// NEVER set marketingOptOut here — preserve existing opt-out status
// Only update fields that don't affect consent state
},
})
}
// Before any bulk send, always cross-reference the suppression list
const suppressed = await db.subscriber.findMany({
where: { marketingOptOut: true },
select: { email: true },
})
const suppressedEmails = new Set(suppressed.map(s => s.email))
const recipients = allRecipients.filter(r => !suppressedEmails.has(r.email))