this repo has no description
at main 5.6 kB view raw
1// Apps Script wrapper - imports the main classifier 2// This file is bundled into a single .gs file 3 4import { classifyEmail } from "../classifier"; 5import type { EmailInput, ClassificationResult } from "../types"; 6 7// Configuration 8const AUTO_LABEL_NAME = "College/Auto"; 9const FILTERED_LABEL_NAME = "College/Filtered"; 10const APPROVED_LABEL_NAME = "College"; 11const DRY_RUN = true; 12 13const MAX_THREADS_PER_RUN = 75; 14const MAX_EXECUTION_TIME_MS = 4.5 * 60 * 1000; 15const GMAIL_BATCH_SIZE = 20; 16 17// Declare global for Apps Script 18declare const GmailApp: any; 19declare const Logger: any; 20declare const Utilities: any; 21declare const ScriptApp: any; 22 23// Main entry points 24function ensureLabels(): void { 25 getOrCreateLabel(AUTO_LABEL_NAME); 26 getOrCreateLabel(FILTERED_LABEL_NAME); 27 getOrCreateLabel(APPROVED_LABEL_NAME); 28 Logger.log(`Labels ensured: ${AUTO_LABEL_NAME}, ${FILTERED_LABEL_NAME}, ${APPROVED_LABEL_NAME}`); 29} 30 31function runTriage(): void { 32 const startTime = Date.now(); 33 const autoLabel = getOrCreateLabel(AUTO_LABEL_NAME); 34 const filteredLabel = getOrCreateLabel(FILTERED_LABEL_NAME); 35 const approvedLabel = getOrCreateLabel(APPROVED_LABEL_NAME); 36 37 const threads = autoLabel.getThreads(0, MAX_THREADS_PER_RUN); 38 if (!threads.length) { 39 Logger.log("No threads under College/Auto."); 40 return; 41 } 42 43 Logger.log(`Processing ${threads.length} threads`); 44 45 let stats = { 46 wouldInbox: 0, 47 wouldFiltered: 0, 48 didInbox: 0, 49 didFiltered: 0, 50 errors: 0, 51 skipped: 0 52 }; 53 54 for (let i = 0; i < threads.length; i++) { 55 const elapsed = Date.now() - startTime; 56 if (elapsed > MAX_EXECUTION_TIME_MS) { 57 Logger.log(`Time limit reached. Processed ${i}/${threads.length}`); 58 stats.skipped = threads.length - i; 59 break; 60 } 61 62 const thread = threads[i]; 63 64 try { 65 processThread(thread, autoLabel, approvedLabel, filteredLabel, stats); 66 } catch (e) { 67 Logger.log(`ERROR: ${e}. FAIL-SAFE: Moving to inbox.`); 68 stats.errors += 1; 69 70 if (!DRY_RUN) { 71 thread.removeLabel(autoLabel); 72 thread.removeLabel(filteredLabel); 73 thread.moveToInbox(); 74 stats.didInbox += 1; 75 } else { 76 stats.wouldInbox += 1; 77 } 78 } 79 80 if ((i + 1) % GMAIL_BATCH_SIZE === 0) { 81 Utilities.sleep(100); 82 } 83 } 84 85 const totalTime = ((Date.now() - startTime) / 1000).toFixed(2); 86 Logger.log(`Summary: Inbox=${stats.wouldInbox}/${stats.didInbox}, Filtered=${stats.wouldFiltered}/${stats.didFiltered}, Errors=${stats.errors}, Time=${totalTime}s`); 87} 88 89function processThread( 90 thread: any, 91 autoLabel: any, 92 approvedLabel: any, 93 filteredLabel: any, 94 stats: any 95): void { 96 const msg = thread.getMessages()[thread.getMessages().length - 1]; 97 if (!msg) throw new Error("No messages in thread"); 98 99 const meta: EmailInput = { 100 subject: safeStr(msg.getSubject()), 101 body: safeStr(msg.getPlainBody(), 10000), 102 from: safeStr(msg.getFrom()), 103 }; 104 105 if (!meta.subject && !meta.body) { 106 Logger.log(`WARNING: No content. FAIL-SAFE: Moving to inbox.`); 107 applyInboxAction(thread, autoLabel, approvedLabel, filteredLabel, stats, "no content"); 108 return; 109 } 110 111 const result = classifyEmail(meta); 112 113 Logger.log(`[${thread.getId()}] Relevant=${result.pertains} Confidence=${result.confidence} Reason="${result.reason}"`); 114 115 if (result.pertains) { 116 applyInboxAction(thread, autoLabel, approvedLabel, filteredLabel, stats, result.reason); 117 } else { 118 applyFilteredAction(thread, autoLabel, filteredLabel, stats, result.reason); 119 } 120} 121 122function applyInboxAction( 123 thread: any, 124 autoLabel: any, 125 approvedLabel: any, 126 filteredLabel: any, 127 stats: any, 128 reason: string 129): void { 130 if (DRY_RUN) { 131 stats.wouldInbox += 1; 132 Logger.log(` DRY_RUN: Would move to Inbox (${reason})`); 133 } else { 134 thread.removeLabel(autoLabel); 135 thread.removeLabel(filteredLabel); 136 thread.addLabel(approvedLabel); 137 thread.moveToInbox(); 138 stats.didInbox += 1; 139 Logger.log(` Applied: Moved to Inbox (${reason})`); 140 } 141} 142 143function applyFilteredAction( 144 thread: any, 145 autoLabel: any, 146 filteredLabel: any, 147 stats: any, 148 reason: string 149): void { 150 if (DRY_RUN) { 151 stats.wouldFiltered += 1; 152 Logger.log(` DRY_RUN: Would filter (${reason})`); 153 } else { 154 thread.removeLabel(autoLabel); 155 thread.addLabel(filteredLabel); 156 if (thread.isInInbox()) thread.moveToArchive(); 157 stats.didFiltered += 1; 158 Logger.log(` Applied: Filtered (${reason})`); 159 } 160} 161 162// Utilities 163function getOrCreateLabel(name: string): any { 164 return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name); 165} 166 167function safeStr(s: string | null, maxLen?: number): string { 168 if (s === null || s === undefined) return ""; 169 const str = s.toString().trim(); 170 if (maxLen && str.length > maxLen) return str.slice(0, maxLen); 171 return str; 172} 173 174function setupTriggers(): void { 175 // Delete existing triggers 176 const triggers = ScriptApp.getProjectTriggers(); 177 for (let i = 0; i < triggers.length; i++) { 178 if (triggers[i].getHandlerFunction() === "runTriage") { 179 ScriptApp.deleteTrigger(triggers[i]); 180 } 181 } 182 183 // Create new trigger 184 ScriptApp.newTrigger("runTriage") 185 .timeBased() 186 .everyMinutes(10) 187 .create(); 188 189 Logger.log("Trigger created: runTriage every 10 minutes"); 190} 191 192// Export for Apps Script global scope - these become top-level functions 193// Note: The bundler needs to be configured to expose these properly 194 195// Make sure functions are not tree-shaken by referencing them 196export { ensureLabels, runTriage, setupTriggers };