import type { ListmonkClient } from "@listmonk-ops/openapi"; export type SubscriberHygieneMode = "winback" | "sunset"; /** A Listmonk list's opt-in mode (`lists.optin`). */ export type ListOptinMode = "single" | "double"; /** The hygiene mutation a failure summary refers to. */ export type SubscriberHygieneMutationEffect = "list_add" | "blocklist"; /** * Bounded mutation failure code; it never carries remote error text. * `http_` is a 4xx/5xx error envelope, `request_failed` a rejected * request or an error envelope without an HTTP error status, and * `negative_acknowledgement` a response without Listmonk's explicit * `data: true` acknowledgement. */ export type SubscriberHygieneMutationErrorCode = `http_${number}` | "request_failed" | "negative_acknowledgement"; export interface SubscriberHygieneOptions { mode?: SubscriberHygieneMode; /** * Minimum age in days of the subscriber profile's updated_at. Listmonk * advances updated_at on profile edits and API blocklisting only — * sends, opens, clicks, opt-in confirmations, unsubscribes, and list * additions leave it untouched — so this selects profiles nobody has * modified, not readers who stopped engaging. */ inactivityDays?: number; /** * Only memberships on these lists count toward eligibility. Without it, * any list membership counts. Either way a candidate needs a membership * that permits delivery (see {@link membershipPermitsDelivery}). */ sourceListIds?: number[]; targetListId?: number; /** * Blocklist sunset candidates. Irreversible for list subscriptions: * Listmonk marks every membership unsubscribed, and removing the * blocklist does not restore them. */ blocklist?: boolean; /** Exact candidate set reported by a dry run; destructive runs process exactly this set. */ subscriberIds?: readonly number[]; /** * Generation guard: the updated_at each selected subscriber carried when * the dry run reported it. Listmonk advances updated_at on profile edits * and API blocklisting, so a guarded destructive retry skips subscribers * its own first attempt blocklisted as well as ones whose profile changed * externally. A list add leaves updated_at unchanged; the retry skips an * already-present target membership structurally instead. */ expectedUpdatedAt?: ReadonlyMap; dryRun?: boolean; maxSubscribers?: number; } export interface SubscriberHygieneResult { mode: SubscriberHygieneMode; cutoffAt: string; dryRun: boolean; totalSubscribersScanned: number; candidateSubscribers: number; /** Subscribers whose requested mutations Listmonk all acknowledged. */ processedSubscribers: number; /** * Subscribers with a failed or unacknowledged mutation, including a * partially applied one (list add landed, blocklist failed). */ failedSubscribers: number; skippedDueToLimit: number; /** Selected subscribers skipped because their updated_at moved past the echoed guard. */ skippedGuarded: number; /** * Guard-eligible subscribers who needed no mutation because an active * target-list membership already existed or an unsubscribed target * membership was deliberately left unchanged to preserve consent. Counted * only when no other mutation was requested. In a destructive run, * processed + failed + skippedAlreadyApplied + skippedGuarded equals the * selected set. */ skippedAlreadyApplied: number; /** The selected subscriber ids — echo them for the destructive run. */ subscriberIds: number[]; /** * Parallel to subscriberIds: the raw updated_at each selected subscriber * carried at observation. Pair them in order as the destructive run's * subscriber_guards so a retry skips anyone that moved. */ subscriberUpdatedAt: string[]; targetListId?: number; blocklist: boolean; sample: Array<{ emailMasked: string; updated_at?: string; }>; /** * Warnings plus one bounded summary per failed effect and code, e.g. * `Subscriber mutation failed for 2 subscribers: list_add http_403`. */ errors: string[]; } /** * Mask an email address for safe display in results (e.g. `j***@example.com`). * Never returns the raw email. */ export declare function maskEmail(email: string): string; /** * Listmonk's regular-campaign delivery rule for one list membership * (`next-campaign-subscribers`): an unsubscribed membership never receives * mail, a double opt-in list delivers only to confirmed members, and a * single opt-in list also delivers to unconfirmed ones. An unknown * subscription status or an undeterminable opt-in mode fails closed. The * sequence consent check (sequence-consent.ts) applies the same rule to * each required list; keep the two in step. */ export declare function membershipPermitsDelivery(subscriptionStatus: unknown, optin: ListOptinMode | undefined): boolean; /** * Read each list's opt-in mode from the full list rows of the list * endpoint — the authority the sequence consent check uses too — rather * than trusting the list row Listmonk embeds in subscriber payloads. A * list the token cannot read, or a row without a recognizable opt-in mode, * is absent from the map, so its unconfirmed memberships fail closed (and * the run reports how many subscribers that skipped); a failed read * rejects the run before anything is selected or mutated. */ export declare function loadListOptinModes(client: ListmonkClient): Promise>; /** * Classify one hygiene mutation response. The Listmonk client does not * throw on HTTP errors, so a 403 or 500 resolves as an `{ error }` envelope * instead of rejecting; only the explicit `data: true` acknowledgement * counts as applied. Returns undefined when acknowledged. The code is * derived from the HTTP status alone and never copies remote error text. */ export declare function classifyHygieneMutationResponse(response: unknown): SubscriberHygieneMutationErrorCode | undefined; export declare function runSubscriberHygiene(client: ListmonkClient, options?: SubscriberHygieneOptions): Promise;