/// /** * Gmail Label Manager Pattern * * Add or remove labels from emails with mandatory user confirmation. * * Security: User must see the exact label changes and explicitly confirm * before any modification. This pattern can serve as a declassification * gate when policies are implemented (patterns with verified SHA can be trusted). * * Usage: * 1. Create and favorite a Google Auth piece with "Gmail (add/remove labels)" permission * 2. Create a Gmail Label Manager piece * 3. Set the messageIds to modify (single ID or array of IDs) * 4. Select labels to add/remove * 5. Review the confirmation dialog * 6. Confirm to apply changes * * This pattern works well linked to a Gmail Importer - select emails there, * then manage their labels here. * * Multi-account support: Use createGoogleAuth() with accountType parameter * to wish for #googleAuthPersonal or #googleAuthWork accounts. * See: gmail-importer.tsx for an example with account switching dropdown. */ import { Default, derive, handler, ifElse, NAME, pattern, UI, Writable, } from "commontools"; import { type GmailLabel, GmailSendClient, type ModifyLabelsParams, } from "../util/gmail-send-client.ts"; import { type Auth, createGoogleAuth, type ScopeKey, } from "../util/google-auth-manager.tsx"; // ============================================================================ // TYPES // ============================================================================ type LabelOperation = { /** Message IDs to modify */ messageIds: string[]; /** Label IDs to add */ addLabelIds: string[]; /** Label IDs to remove */ removeLabelIds: string[]; /** Human-readable label names (for display) */ addLabelNames: string[]; removeLabelNames: string[]; }; type OperationResult = { success: boolean; messageCount: number; error?: string; timestamp?: string; }; interface Input { /** Message ID(s) to manage labels for - can be single string or array */ messageIds: Default; /** Labels to add (by ID) */ labelsToAdd: Default; /** Labels to remove (by ID) */ labelsToRemove: Default; } /** Gmail label manager with confirmation. #gmailLabelManager */ interface Output { messageIds: string[]; labelsToAdd: string[]; labelsToRemove: string[]; result: OperationResult | null; /** Available labels (fetched from Gmail) */ availableLabels: GmailLabel[]; } // ============================================================================ // HANDLERS // ============================================================================ const fetchLabels = handler< unknown, { auth: Writable; availableLabels: Writable; loadingLabels: Writable; } >(async (_, { auth, availableLabels, loadingLabels }) => { loadingLabels.set(true); try { const client = new GmailSendClient(auth, { debugMode: true }); const labels = await client.listLabels(); // Sort: user labels first (alphabetically), then system labels labels.sort((a, b) => { if (a.type !== b.type) { return a.type === "user" ? -1 : 1; } return a.name.localeCompare(b.name); }); availableLabels.set(labels); } catch (error) { console.error("[GmailLabelManager] Failed to fetch labels:", error); } finally { loadingLabels.set(false); } }); const toggleAddLabel = handler< unknown, { labelsToAdd: Writable; labelId: string } >((_, { labelsToAdd, labelId }) => { const current = labelsToAdd.get(); if (current.includes(labelId)) { labelsToAdd.set(current.filter((id) => id !== labelId)); } else { labelsToAdd.set([...current, labelId]); } }); const toggleRemoveLabel = handler< unknown, { labelsToRemove: Writable; labelId: string } >((_, { labelsToRemove, labelId }) => { const current = labelsToRemove.get(); if (current.includes(labelId)) { labelsToRemove.set(current.filter((id) => id !== labelId)); } else { labelsToRemove.set([...current, labelId]); } }); const prepareOperation = handler< unknown, { messageIds: Writable; labelsToAdd: Writable; labelsToRemove: Writable; availableLabels: Writable; pendingOp: Writable; } >( ( _, { messageIds, labelsToAdd, labelsToRemove, availableLabels, pendingOp }, ) => { const ids = messageIds.get(); const add = labelsToAdd.get(); const remove = labelsToRemove.get(); const labels = availableLabels.get(); // Map IDs to names for display const labelMap = new Map(labels.map((l) => [l.id, l.name])); pendingOp.set({ messageIds: [...ids], addLabelIds: [...add], removeLabelIds: [...remove], addLabelNames: add.map((id) => labelMap.get(id) || id), removeLabelNames: remove.map((id) => labelMap.get(id) || id), }); }, ); const cancelOperation = handler< unknown, { pendingOp: Writable } >((_, { pendingOp }) => { pendingOp.set(null); }); const confirmOperation = handler< unknown, { pendingOp: Writable; auth: Writable; processing: Writable; result: Writable; labelsToAdd: Writable; labelsToRemove: Writable; } >( async ( _, { pendingOp, auth, processing, result, labelsToAdd, labelsToRemove }, ) => { const op = pendingOp.get(); if (!op) return; processing.set(true); result.set(null); try { const client = new GmailSendClient(auth, { debugMode: true }); const params: ModifyLabelsParams = { addLabelIds: op.addLabelIds.length > 0 ? op.addLabelIds : undefined, removeLabelIds: op.removeLabelIds.length > 0 ? op.removeLabelIds : undefined, }; if (op.messageIds.length === 1) { // Single message - use regular modify await client.modifyLabels(op.messageIds[0], params); } else { // Multiple messages - use batch modify await client.batchModifyLabels(op.messageIds, params); } result.set({ success: true, messageCount: op.messageIds.length, timestamp: new Date().toISOString(), }); pendingOp.set(null); // Clear selections after success labelsToAdd.set([]); labelsToRemove.set([]); } catch (error) { result.set({ success: false, messageCount: op.messageIds.length, error: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString(), }); } finally { processing.set(false); } }, ); const dismissResult = handler< unknown, { result: Writable } >((_, { result }) => { result.set(null); }); // ============================================================================ // PATTERN // ============================================================================ export default pattern( ({ messageIds, labelsToAdd, labelsToRemove }) => { // Auth via createGoogleAuth utility - handles discovery, validation, and UI const { auth, fullUI, isReady } = createGoogleAuth({ requiredScopes: ["gmail", "gmailModify"] as ScopeKey[], }); const hasAuth = isReady; // UI state const availableLabels = Writable.of([]); const loadingLabels = Writable.of(false); const pendingOp = Writable.of(null); const processing = Writable.of(false); const result = Writable.of(null); // Computed const messageCount = derive(messageIds, (ids) => ids.length); const hasMessages = derive(messageIds, (ids) => ids.length > 0); const hasChanges = derive( { labelsToAdd, labelsToRemove }, ({ labelsToAdd, labelsToRemove }) => labelsToAdd.length > 0 || labelsToRemove.length > 0, ); const canApply = derive( { hasAuth, hasMessages, hasChanges, processing }, ({ hasAuth, hasMessages, hasChanges, processing }) => hasAuth && hasMessages && hasChanges && !processing, ); // Common system labels that are useful (prefixed with _ as not currently used) const _systemLabelIds = [ "INBOX", "STARRED", "IMPORTANT", "UNREAD", "SPAM", "TRASH", ]; return { [NAME]: "Gmail Label Manager", [UI]: (

Gmail Label Manager

{/* Auth status - handled by createGoogleAuth utility */} {fullUI} {/* Refresh labels button - protected until authenticated */} {ifElse( isReady,
, null, )} {/* Result display */} {ifElse( derive(result, (r: OperationResult | null) => r?.success === true),
Labels Updated Successfully!
Modified {derive(result, (r: OperationResult | null) => r?.messageCount)} message(s)
, null, )} {ifElse( derive(result, (r: OperationResult | null) => r?.success === false),
Failed to Update Labels
{derive(result, (r: OperationResult | null) => r?.error)}
, null, )} {/* Message count indicator */}
{ifElse( hasMessages, {messageCount} message(s) selected for label changes , No messages selected. Link messageIds from a Gmail Importer. , )}
{/* Label selection */} {ifElse( derive(availableLabels, (l: GmailLabel[]) => l.length > 0),
{/* Add labels section */}
+ Add Labels
{derive(availableLabels, (labels) => labels.map((label) => { const isSelected = derive(labelsToAdd, (add) => add.includes(label.id)); const isInRemove = derive(labelsToRemove, (rem) => rem.includes(label.id)); return ( ); }))}
{/* Remove labels section */}
− Remove Labels
{derive(availableLabels, (labels) => labels.map((label) => { const isSelected = derive(labelsToRemove, (rem) => rem.includes(label.id)); const isInAdd = derive(labelsToAdd, (add) => add.includes(label.id)); return ( ); }))}
,
{ifElse( hasAuth, Click "Refresh Labels" above to load your Gmail labels. , Authenticate to load labels., )}
, )} {/* Apply button */} {/* CONFIRMATION DIALOG */} {ifElse( pendingOp,
{/* Header */}
🏷️

Confirm Label Changes

{/* Content */}
Modifying{" "} {derive(pendingOp, (op: LabelOperation | null) => op?.messageIds.length || 0)} {" "} message(s)
{/* Labels to add */} {ifElse( derive( pendingOp, (op: LabelOperation | null) => (op?.addLabelNames.length || 0) > 0, ),
+ Adding:
{derive(pendingOp, (op: LabelOperation | null) => (op?.addLabelNames || []).map((name: string) => ( {name} )))}
, null, )} {/* Labels to remove */} {ifElse( derive( pendingOp, (op: LabelOperation | null) => (op?.removeLabelNames.length || 0) > 0, ),
− Removing:
{derive(pendingOp, (op: LabelOperation | null) => (op?.removeLabelNames || []).map((name: string) => ( {name} )))}
, null, )}
{/* Warning */}
This will modify your Gmail labels
The selected labels will be added or removed from the specified messages in your Gmail account.
{/* Footer */}
, null, )}
), messageIds, labelsToAdd, labelsToRemove, result, availableLabels, }; }, );