/// /** * Email Pattern Launcher * * Automatically discovers and launches relevant email-based patterns * based on incoming Gmail messages. * * Flow: * 1. Fetches pattern registry JSON (maps email addresses to patterns) * 2. Builds Gmail query from all registered email patterns * 3. Uses GmailImporter to fetch matching emails * 4. Matches emails to patterns by 'from' address * 5. Launches each matched pattern via fetchAndRunPattern * 6. Renders pattern previews with navigation links * * Usage: * 1. Deploy a google-auth piece and complete OAuth * 2. Deploy this pattern * 3. Link: ct piece link google-auth/auth email-pattern-launcher/overrideAuth */ import { //compileAndRun, computed, derive, fetchData, //fetchProgram, NAME, navigateTo, pattern, UI, when, } from "commontools"; import GmailExtractor, { type Auth } from "../core/gmail-extractor.tsx"; import USPSInformedDeliveryPattern from "./usps-informed-delivery.tsx"; import BerkeleyLibraryPattern from "./berkeley-library.tsx"; import ChaseBillPattern from "./chase-bill-tracker.tsx"; import BAMSchoolDashboardPattern from "./bam-school-dashboard.tsx"; import BofABillTrackerPattern from "./bofa-bill-tracker.tsx"; import EmailTicketFinderPattern from "./email-ticket-finder.tsx"; import CalendarChangeDetectorPattern from "./calendar-change-detector.tsx"; import EmailNotesPattern from "./email-notes.tsx"; import UnitedFlightTrackerPattern from "./united-flight-tracker.tsx"; const PATTERNS: any = { "google/extractors/usps-informed-delivery.tsx": USPSInformedDeliveryPattern, "google/extractors/berkeley-library.tsx": BerkeleyLibraryPattern, "google/extractors/chase-bill-tracker.tsx": ChaseBillPattern, "google/extractors/bam-school-dashboard.tsx": BAMSchoolDashboardPattern, "google/extractors/bofa-bill-tracker.tsx": BofABillTrackerPattern, "google/extractors/email-ticket-finder.tsx": EmailTicketFinderPattern, "google/extractors/calendar-change-detector.tsx": CalendarChangeDetectorPattern, "google/extractors/email-notes.tsx": EmailNotesPattern, "google/extractors/united-flight-tracker.tsx": UnitedFlightTrackerPattern, }; // ============================================================================= // TYPES // ============================================================================= /** Registry entry mapping a pattern to email address patterns */ interface RegistryEntry { /** Path to the pattern file (relative to /api/patterns/) */ patternUri: string; /** Glob-style email patterns (e.g., "*@usps.com") */ emailPatterns: string[]; } /** Info about a pattern that matched emails */ interface PatternMatchInfo { /** Path to the pattern file */ patternUri: string; /** The full registry entry */ entry: RegistryEntry; /** Email addresses that triggered this pattern */ matchedEmails: string[]; } // ============================================================================= // HELPERS // ============================================================================= /** * Check if an email address matches a glob pattern. * Supports wildcards: "*@domain.com" matches any email at that domain. */ function matchesEmailPattern(email: string, pattern: string): boolean { if (!email || !pattern) return false; const emailLower = email.toLowerCase(); const patternLower = pattern.toLowerCase(); // Convert glob pattern to regex // * matches anything before @, and @ and . are literal const regexPattern = patternLower .replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape special regex chars except * .replace(/\*/g, ".*"); // Convert * to .* const regex = new RegExp(`${regexPattern}$`, "i"); return regex.test(emailLower); } /** * Build Gmail query from email patterns. */ function buildGmailQuery(entries: RegistryEntry[]): string { // Build "from:@domain1 OR from:@domain2 ..." query const parts = entries.filter(Boolean).flatMap((entry) => entry.emailPatterns.filter(Boolean).map((pattern) => `from:${pattern}`) ); return parts.join(" OR "); } // ============================================================================= // PATTERN // ============================================================================= interface PatternInput { // Optional: Link auth directly from a Google Auth piece // Use: ct piece link googleAuthPiece/auth emailPatternLauncher/overrideAuth overrideAuth?: Auth; } /** Email pattern launcher that discovers and runs relevant patterns. #emailPatternLauncher */ interface PatternOutput { matchedPatterns: unknown[]; emailCount: number; matchCount: number; previewUI: unknown; } export default pattern(({ overrideAuth }) => { // ========================================================================== // FETCH REGISTRY // ========================================================================== const registryFetch = fetchData({ url: "/api/patterns/google/extractors/email-pattern-registry.json", mode: "json", }); const registry = computed(() => registryFetch.result || []); const registryError = computed(() => registryFetch.error); const registryLoading = computed(() => registryFetch.pending); // ========================================================================== // BUILD GMAIL QUERY AND FETCH EMAILS // ========================================================================== // Build combined Gmail query from all registry patterns const gmailQuery = computed(() => { const entries = registry; if (!entries || entries.length === 0) return ""; return buildGmailQuery(entries); }); // Instantiate GmailExtractor with the combined query (raw mode - no extraction) const extractor = GmailExtractor({ gmailQuery, limit: 100, overrideAuth, }); const allEmails = extractor.emails; const emailCount = extractor.emailCount; // ========================================================================== // MATCH EMAILS TO PATTERNS // ========================================================================== // Find which patterns have matching emails - returns array of matches const patternMatches = computed(() => { const matchMap = new Map< string, { entry: RegistryEntry; emails: Set } >(); const entries = registry; const emails = allEmails || [] as any[]; if (!entries || entries.length === 0) return []; for (const email of emails) { const fromAddress = email?.from; if (!fromAddress) continue; for (const entry of entries) { if (!entry) continue; for (const emailPattern of entry.emailPatterns) { if (matchesEmailPattern(fromAddress, emailPattern)) { const key = entry.patternUri; if (!matchMap.has(key)) { matchMap.set(key, { entry, emails: new Set() }); } matchMap.get(key)!.emails.add(fromAddress); break; // Only match once per entry } } } } // Convert Map to array const results: PatternMatchInfo[] = []; for (const [patternUri, { entry, emails }] of matchMap) { results.push({ patternUri, entry, matchedEmails: Array.from(emails), }); } return results; }); const matchCount = computed(() => patternMatches?.length || 0); // ========================================================================== // LAUNCH MATCHED PATTERNS // ========================================================================== // Launch each matched pattern - use .map() for reactive pattern instantiation const launchedPatterns = patternMatches.map((matchInfo) => { /* const url = computed(() => `/api/patterns/${matchInfo.patternUri}`); // Fetch the pattern program const programFetch = fetchProgram({ url }); // Use computed to safely handle when program is undefined/pending // Filter out undefined elements to handle race condition where array proxy // pre-allocates with undefined before populating elements const compileParams = computed(() => ({ // Note: Type predicate removed - doesn't work with OpaqueCell types after transformation files: (programFetch.result?.files ?? []).filter( (f) => f !== undefined && f !== null && typeof f.name === "string", ), main: programFetch.result?.main ?? "", input: { overrideAuth }, })); // Compile and run the pattern const compiled = compileAndRun(compileParams); */ const compiled = { result: derive(matchInfo.patternUri, (patternUri) => { const pattern = PATTERNS[patternUri]; if (!pattern) return null; return pattern({} as any).for(patternUri); }), }; return { patternUri: matchInfo.patternUri, entry: matchInfo.entry, matchedEmails: matchInfo.matchedEmails, pending: false, /*computed( () => programFetch.pending || compiled.pending, ),*/ error: null, /* error: computed( () => programFetch.error || compiled.error, ),*/ result: compiled.result, }; }); // Preview UI for compact display const previewUI = (
{matchCount}
Email Patterns
{matchCount} active patterns ยท {emailCount} emails scanned
); return { [NAME]: "Email Pattern Launcher", matchedPatterns: launchedPatterns, emailCount, matchCount, previewUI, [UI]: (
Email Pattern Launcher
{/* Auth UI from GmailExtractor */} {extractor.ui.authStatusUI} {/* Status Section */}
{matchCount}
Active Patterns
{emailCount}
Emails Scanned
{computed(() => registry.length)}
Registered Patterns
{/* Fetch Button */} {/* Registry Error */} {when( registryError,
Error loading registry:{" "} {computed(() => console.log("registryError 2", registryError) ) && ""}
{JSON.stringify(registryError, null, 2)}
, )} {/* Registry Loading */} {registryLoading && (
Loading pattern registry...
)} {/* No Matches Message */} {computed(() => !registryLoading && emailCount > 0 && matchCount === 0 ) && (
No Matching Patterns Found
Scanned {emailCount}{" "} emails but no registered patterns matched.
)} {/* Matched Patterns Section */} {matchCount > 0 && (

Active Email Patterns

{launchedPatterns.map((patternInfo) => (
{/* Pattern Header */}
{patternInfo.patternUri}
Matched: {computed(() => (patternInfo.matchedEmails || []).join(", ") )}
{/* Navigate Button */}
{/* Loading State */} {patternInfo.pending && (
Loading pattern...
)} {/* Error State */} {patternInfo.error && (
Error: {patternInfo.error}
)} {/* Preview UI from launched pattern */} {computed(() => patternInfo.result && !patternInfo.pending && !patternInfo.error ) && (
{/* Render the pattern's previewUI if available */} { /**/ } {patternInfo.result.previewUI}
)}
))}
)} {/* Debug: Gmail Query */}
Debug Info
Gmail Query:
{gmailQuery}
Registered Patterns:
{registry.map((entry: RegistryEntry) => (
{entry.patternUri}: {entry.emailPatterns.join(", ")}
))}
), }; });