this repo has no description

feat: fix the bundler

dunkirk.sh 3c848cc9 1a4b8da6

verified
+26 -18
README.md
···
Intelligent email classifier that automatically filters college marketing spam while keeping important emails in your inbox.
-
**Current Performance**: 100% accuracy on 58 labeled emails
+
**Current Performance**: 100% accuracy on 63 labeled emails
## Quick Start
···
**TypeScript → Google Apps Script Pipeline**
```
-
src/apps-script/Code.ts (TypeScript with type safety)
+
src/classifier.ts (Core classification logic - single source of truth)
+
+
↓ imported by
-
↓ bun run gas build (compile)
+
src/apps-script/wrapper.ts (Apps Script wrapper)
-
build/Code.gs (Google Apps Script)
+
↓ bun run gas (bundle with esbuild)
+
+
build/Code.gs (Single-file Google Apps Script)
↓ Manual copy/paste
Google Apps Script → Gmail Auto-Filtering
```
+
+
**Key Improvement**: The classifier logic is now shared between local testing and Apps Script deployment. No more manual syncing!
## Gmail Deployment
···
# 3. Import and evaluate
bun run import new-emails-labeled.json
-
# 4. Update patterns in src/apps-script/Code.ts
+
# 4. Update patterns in src/classifier.ts
# 5. Test locally
bun test
···
### Adding New Patterns
-
1. Edit `src/apps-script/Code.ts` with type-safe TypeScript
+
1. Edit `src/classifier.ts` - the single source of truth
2. Add tests in `src/classifier.test.ts`
3. Run `bun test` to verify
4. Build and deploy: `bun run gas build` then copy to Apps Script
-
**Note**: Keep `src/classifier.ts` (local testing) and `src/apps-script/Code.ts` (deployed) in sync manually.
+
**Note**: The Apps Script is automatically bundled from `src/classifier.ts` via `src/apps-script/wrapper.ts`
## Project Structure
```
src/
apps-script/
-
Code.ts - Apps Script source (TypeScript)
-
appsscript.json - Apps Script manifest
-
classifier.ts - Core classifier (for local testing)
+
wrapper.ts - Apps Script wrapper (imports classifier)
+
Code.ts - DEPRECATED (see wrapper.ts)
+
appsscript.json - Apps Script manifest
+
classifier.ts - Core classifier (SINGLE SOURCE OF TRUTH)
classifier.test.ts - Unit tests
types.ts - TypeScript types
evaluate.ts - Evaluation tool
label.ts - Interactive labeling CLI
import-labeled.ts - Import labeled emails
-
build-gas.ts - Build/deploy script
+
build-gas.ts - Build/deploy script (uses esbuild)
scripts/
export-from-label.gs - Export emails from Gmail
build/ - Generated (gitignored)
-
Code.gs - Compiled Apps Script
-
compiled/ - Intermediate JavaScript
+
Code.gs - Bundled Apps Script
+
bundled.js - Intermediate bundle
data/
-
labeled-emails.json - Main dataset (58 emails)
+
labeled-emails.json - Main dataset (63 emails)
example-export.json - Example export
tsconfig.apps-script.json - TypeScript config for Apps Script
···
- **Modern syntax**: ES6+ features (arrow functions, classes, etc.)
- **Local development**: Edit with VS Code autocomplete
- **Manual deployment**: Build locally, copy/paste to Apps Script
-
- **No bundler overhead**: Simple TypeScript → JavaScript compilation
+
- **Bundling**: esbuild bundles classifier + wrapper into single file
+
- **Single source of truth**: `src/classifier.ts` used by both local tests and Apps Script
Configuration:
-
- `tsconfig.apps-script.json` - Targets ES2015, no modules
-
- `src/build-gas.ts` - Build script
+
- `src/build-gas.ts` - Build script with esbuild bundler
+
- `src/apps-script/wrapper.ts` - Apps Script wrapper that imports classifier
## Metrics
-
- **Accuracy**: 100% (58/58 emails correctly classified)
+
- **Accuracy**: 100% (63/63 emails correctly classified)
- **Precision**: 100% (no false positives - no spam in inbox)
- **Recall**: 100% (no false negatives - all important emails reach inbox)
- **F1 Score**: 100%
+55
bun.lock
···
"devDependencies": {
"@google/clasp": "^3.1.3",
"@types/google-apps-script": "^2.0.8",
+
"esbuild": "^0.27.1",
"ts2gas": "^4.2.0",
"typescript": "^5.9.3",
},
···
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="],
+
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="],
+
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="],
+
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="],
+
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="],
+
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="],
+
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="],
+
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="],
+
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="],
+
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="],
+
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="],
+
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="],
+
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="],
+
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="],
+
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="],
+
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="],
+
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="],
+
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="],
+
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="],
+
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="],
+
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="],
+
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="],
+
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="],
+
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="],
+
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="],
+
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="],
"@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="],
···
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+
"esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+88 -3
data/labeled-emails.json
···
{
-
"exported_at": "2025-12-07T16:36:04.975Z",
-
"total_count": 58,
+
"exported_at": "2025-12-08T17:47:25.257Z",
+
"total_count": 63,
"label": "College/Auto",
"emails": [
{
···
"reason": "Marketing/unsolicited outreach",
"confidence": "high",
"labeled_at": "2025-12-07T16:36:04.975Z"
+
},
+
{
+
"thread_id": "19afeb02bf7cfb20",
+
"subject": "Re: Your admission decision at Saint Xavier University",
+
"from": "Saint Xavier University <SaintXavierUniversity_at_gosaintxavier.org_l9k069g0@duck.com>",
+
"to": "Kieran Klukas <l9k069g0@duck.com>",
+
"cc": "",
+
"date": "2025-12-08T15:38:56.000Z",
+
"body": "Your status is proof that you've impressed our admission team ...\r\n\r\n<div class=\"dynamic-content\"><p>Dear Kieran,</p><p>Your <strong>Priority Student&nbsp;</strong>status is proof that you've impressed our admission team at Saint Xavier University, and the advantages that come with that status demonstrate that we're interested in your candidacy.</p><p>If you haven't yet done so, <a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEwZGQ3NDkwMjM0ODk5OTkyIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzMzO3M6NDoibGVhZCI7czo3OiI0MDQ4MzkxIjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPTVmMTY2YWVhLWQzNTktNGYyZS05MGIwLWQ2YTViNTFjYzU5NyI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">submit the <strong>Priority Student Application</strong> we've created for you here</a> or <a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEwZGQ3NDkwMjM0ODk5OTkyIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzMzO3M6NDoibGVhZCI7czo3OiI0MDQ4MzkxIjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9NWYxNjZhZWEtZDM1OS00ZjJlLTkwYjAtZDZhNWI1MWNjNTk3Ijt9&\" rel=\"noopener\" target=\"_blank\">add SXU to the Common App</a>. The deadline to apply is <strong>December 15</strong><strong>,</strong> but that should be no problem. Your advantages are seriously time-saving. You'll:</p><ul><li>Receive <strong>an annual scholarship worth up to $24,000</strong> upon admission.</li><li>Pay <strong>no</strong> application fee.</li><li>Write <strong>no</strong> essay (unless you are a nursing applicant).</li><li>Submit <strong>no</strong> test scores unless you want to.</li></ul><p>We want it to be simple to complete our application, Kieran, and we want our top-tier education to be within your reach. That's why we offer a wide range of scholarships and awards to qualified students—and why we're happy to consider you for those awards automatically when you apply.</p><p><a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEwZGQ3NDkwMjM0ODk5OTkyIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzMzO3M6NDoibGVhZCI7czo3OiI0MDQ4MzkxIjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPTVmMTY2YWVhLWQzNTktNGYyZS05MGIwLWQ2YTViNTFjYzU5NyI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">Submit the <strong>Priority Student Application</strong> here.</a></p><p><a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEwZGQ3NDkwMjM0ODk5OTkyIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzMzO3M6NDoibGVhZCI7czo3OiI0MDQ4MzkxIjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9NWYxNjZhZWEtZDM1OS00ZjJlLTkwYjAtZDZhNWI1MWNjNTk3Ijt9&\" rel=\"noopener\" target=\"_blank\">Or add SXU to the Common App here.</a></p><p>I hope you'll take a few moments to submit your application right now, Kieran! I look forward to learning even more about you.</p><p>Sincerely,</p><p><div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 17px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><strong>Brian D. Hotzfield</strong></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 16px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><i>Vice President - Enrollment</i></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">Saint Xavier University</div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">3700 West 103rd Street<br />\r\nChicago, IL 60655</div></p></div>\r\n\r\nWe received your information from\r\nyour Appily college research.\r\n\r\nBrowse to https://my.gosaintxavier.org/email/unsubscribe/6936f10dd7490234899992/l9k069g0@duck.com/058590bcf0737eef4c4d9f35b95dab6a290be2dfa9e70822ef7611cecf12a5fe to no longer receive emails from this company.\r\n",
+
"labels": [
+
"College"
+
],
+
"is_in_inbox": true,
+
"pertains": false,
+
"reason": "Saint Xavier spam",
+
"labeled_at": "2025-12-08T17:47:21.132Z",
+
"confidence": "high"
+
},
+
{
+
"thread_id": "19afea144e0e49d2",
+
"subject": "Re: Your admission decision at Saint Xavier University",
+
"from": "Saint Xavier University <SaintXavierUniversity_at_gosaintxavier.org_3rmhb7wb@duck.com>",
+
"to": "Kieran Klukas <3rmhb7wb@duck.com>",
+
"cc": "",
+
"date": "2025-12-08T15:42:49.000Z",
+
"body": "Your status is proof that you've impressed our admission team ...\r\n\r\n<div class=\"dynamic-content\"><p>Dear Kieran,</p><p>Your <strong>Priority Student&nbsp;</strong>status is proof that you've impressed our admission team at Saint Xavier University, and the advantages that come with that status demonstrate that we're interested in your candidacy.</p><p>If you haven't yet done so, <a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjFmNzk3YzQ3MzMxMDEyOTEzIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4NTY3O3M6NDoibGVhZCI7czo3OiI0MDk2NDI5IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPTJkYTZhZGRlLTRmODQtNDQ1OC1hZGZjLTgzZDVjNWQ4YjNjNyI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">submit the <strong>Priority Student Application</strong> we've created for you here</a> or <a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjFmNzk3YzQ3MzMxMDEyOTEzIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4NTY3O3M6NDoibGVhZCI7czo3OiI0MDk2NDI5IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9MmRhNmFkZGUtNGY4NC00NDU4LWFkZmMtODNkNWM1ZDhiM2M3Ijt9&\" rel=\"noopener\" target=\"_blank\">add SXU to the Common App</a>. The deadline to apply is <strong>December 15</strong><strong>,</strong> but that should be no problem. Your advantages are seriously time-saving. You'll:</p><ul><li>Receive <strong>an annual scholarship worth up to $24,000</strong> upon admission.</li><li>Pay <strong>no</strong> application fee.</li><li>Write <strong>no</strong> essay (unless you are a nursing applicant).</li><li>Submit <strong>no</strong> test scores unless you want to.</li></ul><p>We want it to be simple to complete our application, Kieran, and we want our top-tier education to be within your reach. That's why we offer a wide range of scholarships and awards to qualified students—and why we're happy to consider you for those awards automatically when you apply.</p><p><a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjFmNzk3YzQ3MzMxMDEyOTEzIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4NTY3O3M6NDoibGVhZCI7czo3OiI0MDk2NDI5IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPTJkYTZhZGRlLTRmODQtNDQ1OC1hZGZjLTgzZDVjNWQ4YjNjNyI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">Submit the <strong>Priority Student Application</strong> here.</a></p><p><a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjFmNzk3YzQ3MzMxMDEyOTEzIjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4NTY3O3M6NDoibGVhZCI7czo3OiI0MDk2NDI5IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9MmRhNmFkZGUtNGY4NC00NDU4LWFkZmMtODNkNWM1ZDhiM2M3Ijt9&\" rel=\"noopener\" target=\"_blank\">Or add SXU to the Common App here.</a></p><p>I hope you'll take a few moments to submit your application right now, Kieran! I look forward to learning even more about you.</p><p>Sincerely,</p><p><div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 17px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><strong>Brian D. Hotzfield</strong></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 16px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><i>Vice President - Enrollment</i></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">Saint Xavier University</div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">3700 West 103rd Street<br />\r\nChicago, IL 60655</div></p></div>\r\n\r\nWe received your information from\r\nthe College Board's Student Search Service.\r\n\r\nBrowse to https://my.gosaintxavier.org/email/unsubscribe/6936f1f797c47331012913/3rmhb7wb@duck.com/ec3770a0580ffa905c5c0aa5eb189f0af3764ad8cb445950c926484d62b8a9a3 to no longer receive emails from this company.\r\n",
+
"labels": [
+
"College"
+
],
+
"is_in_inbox": true,
+
"pertains": false,
+
"reason": "Saint Xavier spam",
+
"labeled_at": "2025-12-08T17:47:21.133Z",
+
"confidence": "high"
+
},
+
{
+
"thread_id": "19afe9e19288e1fd",
+
"subject": "Re: Your admission decision at Saint Xavier University",
+
"from": "Saint Xavier University <SaintXavierUniversity_at_gosaintxavier.org_canopy-rind-aloft@duck.com>",
+
"to": "Kieran Klukas <canopy-rind-aloft@duck.com>",
+
"cc": "",
+
"date": "2025-12-08T15:39:22.000Z",
+
"body": "Your status is proof that you've impressed our admission team ...\r\n\r\n<div class=\"dynamic-content\"><p>Dear Kieran,</p><p>Your <strong>Priority Student&nbsp;</strong>status is proof that you've impressed our admission team at Saint Xavier University, and the advantages that come with that status demonstrate that we're interested in your candidacy.</p><p>If you haven't yet done so, <a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEyNjgzNTJhODk0MzM2MTE0IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzU4O3M6NDoibGVhZCI7czo3OiI0MDQ5ODI4IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPThkZmIyYzRmLTg4NzQtNDdiNC1hYzZiLTZlOWNjMDcxN2MzZiI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">submit the <strong>Priority Student Application</strong> we've created for you here</a> or <a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEyNjgzNTJhODk0MzM2MTE0IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzU4O3M6NDoibGVhZCI7czo3OiI0MDQ5ODI4IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9OGRmYjJjNGYtODg3NC00N2I0LWFjNmItNmU5Y2MwNzE3YzNmIjt9&\" rel=\"noopener\" target=\"_blank\">add SXU to the Common App</a>. The deadline to apply is <strong>December 15</strong><strong>,</strong> but that should be no problem. Your advantages are seriously time-saving. You'll:</p><ul><li>Receive <strong>an annual scholarship worth up to $24,000</strong> upon admission.</li><li>Pay <strong>no</strong> application fee.</li><li>Write <strong>no</strong> essay (unless you are a nursing applicant).</li><li>Submit <strong>no</strong> test scores unless you want to.</li></ul><p>We want it to be simple to complete our application, Kieran, and we want our top-tier education to be within your reach. That's why we offer a wide range of scholarships and awards to qualified students—and why we're happy to consider you for those awards automatically when you apply.</p><p><a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEyNjgzNTJhODk0MzM2MTE0IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzU4O3M6NDoibGVhZCI7czo3OiI0MDQ5ODI4IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPThkZmIyYzRmLTg4NzQtNDdiNC1hYzZiLTZlOWNjMDcxN2MzZiI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">Submit the <strong>Priority Student Application</strong> here.</a></p><p><a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjEyNjgzNTJhODk0MzM2MTE0IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MzU4O3M6NDoibGVhZCI7czo3OiI0MDQ5ODI4IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9OGRmYjJjNGYtODg3NC00N2I0LWFjNmItNmU5Y2MwNzE3YzNmIjt9&\" rel=\"noopener\" target=\"_blank\">Or add SXU to the Common App here.</a></p><p>I hope you'll take a few moments to submit your application right now, Kieran! I look forward to learning even more about you.</p><p>Sincerely,</p><p><div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 17px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><strong>Brian D. Hotzfield</strong></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 16px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><i>Vice President - Enrollment</i></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">Saint Xavier University</div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">3700 West 103rd Street<br />\r\nChicago, IL 60655</div></p></div>\r\n\r\nWe received your information from\r\nyour Appily college research.\r\n\r\nBrowse to https://my.gosaintxavier.org/email/unsubscribe/6936f1268352a894336114/canopy-rind-aloft@duck.com/1e9968b0f7a1a311a836deb2fb20328fd9237987914de2229216101fabe9e410 to no longer receive emails from this company.\r\n",
+
"labels": [
+
"College"
+
],
+
"is_in_inbox": true,
+
"pertains": false,
+
"reason": "Saint Xavier spam",
+
"labeled_at": "2025-12-08T17:47:21.133Z",
+
"confidence": "high"
+
},
+
{
+
"thread_id": "19afe9c44a1c6a1a",
+
"subject": "Re: Your admission decision at Saint Xavier University",
+
"from": "Saint Xavier University <SaintXavierUniversity_at_gosaintxavier.org_pulp-flint-maybe@duck.com>",
+
"to": "Kieran Klukas <pulp-flint-maybe@duck.com>",
+
"cc": "",
+
"date": "2025-12-08T15:37:22.000Z",
+
"body": "Your status is proof that you've impressed our admission team ...\r\n\r\n<div class=\"dynamic-content\"><p>Dear Kieran,</p><p>Your <strong>Priority Student&nbsp;</strong>status is proof that you've impressed our admission team at Saint Xavier University, and the advantages that come with that status demonstrate that we're interested in your candidacy.</p><p>If you haven't yet done so, <a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjBhZmJmODhlMjIyNzI1ODc3IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MjM5O3M6NDoibGVhZCI7czo3OiI0MDM4MTE2IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPTYwNmE0Yjk0LTVjNzgtNDZlMS1hM2IwLTc0NDllYjg2MGI4YiI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">submit the <strong>Priority Student Application</strong> we've created for you here</a> or <a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjBhZmJmODhlMjIyNzI1ODc3IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MjM5O3M6NDoibGVhZCI7czo3OiI0MDM4MTE2IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9NjA2YTRiOTQtNWM3OC00NmUxLWEzYjAtNzQ0OWViODYwYjhiIjt9&\" rel=\"noopener\" target=\"_blank\">add SXU to the Common App</a>. The deadline to apply is <strong>December 15</strong><strong>,</strong> but that should be no problem. Your advantages are seriously time-saving. You'll:</p><ul><li>Receive <strong>an annual scholarship worth up to $24,000</strong> upon admission.</li><li>Pay <strong>no</strong> application fee.</li><li>Write <strong>no</strong> essay (unless you are a nursing applicant).</li><li>Submit <strong>no</strong> test scores unless you want to.</li></ul><p>We want it to be simple to complete our application, Kieran, and we want our top-tier education to be within your reach. That's why we offer a wide range of scholarships and awards to qualified students—and why we're happy to consider you for those awards automatically when you apply.</p><p><a href=\"https://my.gosaintxavier.org/f/r/d782e1d1ced69d7230dc062f2?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjBhZmJmODhlMjIyNzI1ODc3IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MjM5O3M6NDoibGVhZCI7czo3OiI0MDM4MTE2IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDQ6Imh0dHBzOi8vYXBwbGljYXRpb24uZ29zYWludHhhdmllci5vcmcvP3V0bV9jYW1wYWlnbj1kbCZ1dG1fc291cmNlPWVucm9sbDM2MF9hcHBseSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jb250ZW50PWFwcGx5JnRubnQ9MGI4Y2QyNDQtMjQ4OC00MDAxLWEzYWYtYWM0MmU1NTM3YjY0JlVUTV9yY3JkPTYwNmE0Yjk0LTVjNzgtNDZlMS1hM2IwLTc0NDllYjg2MGI4YiI7fQ%3D%3D&\" rel=\"noopener\" target=\"_blank\">Submit the <strong>Priority Student Application</strong> here.</a></p><p><a href=\"https://my.gosaintxavier.org/f/r/0fa03438ddc9ca8c49b9656fc?ct=YTo3OntzOjY6InNvdXJjZSI7YTowOnt9czo1OiJlbWFpbCI7aToyNjA7czo0OiJzdGF0IjtzOjIyOiI2OTM2ZjBhZmJmODhlMjIyNzI1ODc3IjtzOjk6InNlbnRfdGltZSI7aToxNzY1MjA4MjM5O3M6NDoibGVhZCI7czo3OiI0MDM4MTE2IjtzOjc6ImNoYW5uZWwiO2E6MTp7czo1OiJlbWFpbCI7aToyNjA7fXM6MjQ6Im10Y19yZWRpcmVjdF9kZXN0aW5hdGlvbiI7czoyMDY6Imh0dHBzOi8vYXBwbHkuY29tbW9uYXBwLm9yZy9sb2dpbj9tYT05MTg%2FdXRtX2NhbXBhaWduPWRsJnV0bV9zb3VyY2U9ZW5yb2xsMzYwX2FwcGx5JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9YXBwbHkmdG5udD0wYjhjZDI0NC0yNDg4LTQwMDEtYTNhZi1hYzQyZTU1MzdiNjQmVVRNX3JjcmQ9NjA2YTRiOTQtNWM3OC00NmUxLWEzYjAtNzQ0OWViODYwYjhiIjt9&\" rel=\"noopener\" target=\"_blank\">Or add SXU to the Common App here.</a></p><p>I hope you'll take a few moments to submit your application right now, Kieran! I look forward to learning even more about you.</p><p>Sincerely,</p><p><div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 17px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><strong>Brian D. Hotzfield</strong></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-bottom: 0px; text-align: left; line-height: 1.3; font-size: 16px; color: rgb(0, 0, 0); --darkreader-inline-color: #b4b2b0;\"><i>Vice President - Enrollment</i></div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">Saint Xavier University</div>\r\n\r\n<div data-darkreader-inline-color=\"\" style=\"margin-top: 0px; margin-bottom: 0px; color: rgb(0, 0, 0); font-size: 15px; line-height: 1.3; --darkreader-inline-color: #b4b2b0;\">3700 West 103rd Street<br />\r\nChicago, IL 60655</div></p></div>\r\n\r\nWe received your information from\r\nyour Appily college research.\r\n\r\nBrowse to https://my.gosaintxavier.org/email/unsubscribe/6936f0afbf88e222725877/pulp-flint-maybe@duck.com/6d5ec3a37a1794f6a6b9799f1225cc73cc74658dcea93313d2240510a1b1729a to no longer receive emails from this company.\r\n",
+
"labels": [
+
"College"
+
],
+
"is_in_inbox": true,
+
"pertains": false,
+
"reason": "Saint Xavier spam",
+
"labeled_at": "2025-12-08T17:47:21.133Z",
+
"confidence": "high"
+
},
+
{
+
"thread_id": "19afadb81307ff62",
+
"subject": "Drake Scholarships & Financial Aid Opportunities",
+
"from": "Drake University <admission_at_drake.edu_jkgwnjk4@duck.com>",
+
"to": "jkgwnjk4@duck.com",
+
"cc": "",
+
"date": "2025-12-07T22:07:56.000Z",
+
"body": "Sign up now to learn more\r\n\r\n͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌  ͏‌   Drake offers a wide variety of scholarship and financial aid opportunities for you!\r\n\r\n\r\n Hello Kieran, \r\n\r\n The 2026-27 Free Application for Federal Student Aid (FAFSA) is now open. The FAFSA is required to be considered for grants (free money!) as well as eligibility for Federal Work Study or federal student loans. Plus, some scholarship opportunities at Drake also require a FAFSA submission. \r\n\r\n Here are two steps you can take now: \r\n\r\n\t1. File the FAFSA <http://studentaid.gov/h/apply-for-aid/fafsa> with Drake's school code (001860) to receive a financial aid offer this spring\r\n\t2. Join us on Zoom on December 16 for a live, informative virtual program about Drake's scholarships and financial aid opportunities. Details are below----reserve your spot! \r\n\r\nVirtual: Exploring Drake's Scholarship Opportunities and Financial Aid Programs\r\n\r\nTuesday, December 16\r\n 6pm Central Time\r\n Zoom\r\n\r\n In this session, Drake's admission and financial aid experts will provide an overview of the scholarship programs and application processes and the financial aid application process and timeline. \r\n\r\nPlease note: This will be an interactive, live virtual event and will not be recorded so that you and your family can ask your questions candidly! Don't miss it! \r\n\r\nSIGN UP: SCHOLARSHIP & FINANCIAL AID ZOOM <https://apply.drake.edu/register/?id=1e7a4882-83b9-494b-bae0-5859c4beb390&pid=f3xaFlTfujYC3gJSL1vEdMHTYfLD-KHP-c7hKXUECno0iHhYvScvsssoZU99I5BgI_HuVjusiP8Q9x3xTsGbloPaN9SFA8aozApbbG2A7LM>\r\n\r\n Investing in your future takes careful planning. Drake looks forward to assisting you with exploring sources for financial support. \r\n\r\n Go Bulldogs! \r\n\r\n\r\n\r\n\r\n\r\n2507 University Ave. \r\n Des Moines, IA 50311\r\n 515-271-2011 | drake.edu\r\n\r\n\r\n\r\n\r\n\r\n\"You may be receiving this email as an opt-in from the College Board Student Search Service\"\r\n\r\n\r\nThis email was sent to jkgwnjk4@duck.com by Drake University.\r\nDrake University, 2507 University Ave, Des Moines, IA 50311\r\nUnsubscribe from All Drake University Admission Communications. <https://apply.drake.edu/go?r=optout&mid=6cd6393e-5e20-466e-b190-57ba8799f352>\r\n",
+
"labels": [
+
"College"
+
],
+
"is_in_inbox": true,
+
"pertains": false,
+
"reason": "Drake scholarship spam",
+
"labeled_at": "2025-12-08T17:47:21.133Z",
+
"confidence": "high"
}
]
-
}
+
}
+1
package.json
···
"devDependencies": {
"@google/clasp": "^3.1.3",
"@types/google-apps-script": "^2.0.8",
+
"esbuild": "^0.27.1",
"ts2gas": "^4.2.0",
"typescript": "^5.9.3"
}
+9 -441
src/apps-script/Code.ts
···
-
// Apps Script classifier - compiled from TypeScript
-
// This file is the source of truth for Gmail filtering logic
-
-
// Configuration
-
const AUTO_LABEL_NAME = "College/Auto";
-
const FILTERED_LABEL_NAME = "College/Filtered";
-
const APPROVED_LABEL_NAME = "College";
-
const DRY_RUN = true;
-
-
const AI_BASE_URL = "https://ai.hackclub.com/proxy/v1/chat/completions";
-
const AI_MODEL = "deepseek/deepseek-r1-distill-qwen-32b";
-
-
const MAX_THREADS_PER_RUN = 75;
-
const MAX_EXECUTION_TIME_MS = 4.5 * 60 * 1000;
-
const GMAIL_BATCH_SIZE = 20;
-
const AI_CONFIDENCE_THRESHOLD = 0.5;
-
-
// Main entry points
-
function ensureLabels(): void {
-
getOrCreateLabel(AUTO_LABEL_NAME);
-
getOrCreateLabel(FILTERED_LABEL_NAME);
-
getOrCreateLabel(APPROVED_LABEL_NAME);
-
Logger.log(`Labels ensured: ${AUTO_LABEL_NAME}, ${FILTERED_LABEL_NAME}, ${APPROVED_LABEL_NAME}`);
-
}
-
-
function runTriage(): void {
-
const startTime = Date.now();
-
const autoLabel = getOrCreateLabel(AUTO_LABEL_NAME);
-
const filteredLabel = getOrCreateLabel(FILTERED_LABEL_NAME);
-
const approvedLabel = getOrCreateLabel(APPROVED_LABEL_NAME);
-
-
const threads = autoLabel.getThreads(0, MAX_THREADS_PER_RUN);
-
if (!threads.length) {
-
Logger.log("No threads under College/Auto.");
-
return;
-
}
-
-
Logger.log(`Processing ${threads.length} threads`);
-
-
let stats = {
-
wouldInbox: 0,
-
wouldFiltered: 0,
-
didInbox: 0,
-
didFiltered: 0,
-
errors: 0,
-
skipped: 0
-
};
-
-
for (let i = 0; i < threads.length; i++) {
-
const elapsed = Date.now() - startTime;
-
if (elapsed > MAX_EXECUTION_TIME_MS) {
-
Logger.log(`Time limit reached. Processed ${i}/${threads.length}`);
-
stats.skipped = threads.length - i;
-
break;
-
}
-
-
const thread = threads[i];
-
-
try {
-
processThread(thread, autoLabel, approvedLabel, filteredLabel, stats);
-
} catch (e) {
-
Logger.log(`ERROR: ${e}. FAIL-SAFE: Moving to inbox.`);
-
stats.errors += 1;
-
-
if (!DRY_RUN) {
-
thread.removeLabel(autoLabel);
-
thread.removeLabel(filteredLabel);
-
thread.moveToInbox();
-
stats.didInbox += 1;
-
} else {
-
stats.wouldInbox += 1;
-
}
-
}
-
-
if ((i + 1) % GMAIL_BATCH_SIZE === 0) {
-
Utilities.sleep(100);
-
}
-
}
-
-
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
-
Logger.log(`Summary: Inbox=${stats.wouldInbox}/${stats.didInbox}, Filtered=${stats.wouldFiltered}/${stats.didFiltered}, Errors=${stats.errors}, Time=${totalTime}s`);
-
}
-
-
function processThread(
-
thread: GoogleAppsScript.Gmail.GmailThread,
-
autoLabel: GoogleAppsScript.Gmail.GmailLabel,
-
approvedLabel: GoogleAppsScript.Gmail.GmailLabel,
-
filteredLabel: GoogleAppsScript.Gmail.GmailLabel,
-
stats: any
-
): void {
-
const msg = thread.getMessages()[thread.getMessages().length - 1];
-
if (!msg) throw new Error("No messages in thread");
-
-
const meta = {
-
subject: safeStr(msg.getSubject()),
-
body: safeStr(msg.getPlainBody(), 10000),
-
from: safeStr(msg.getFrom()),
-
};
-
-
if (!meta.subject && !meta.body) {
-
Logger.log(`WARNING: No content. FAIL-SAFE: Moving to inbox.`);
-
applyInboxAction(thread, autoLabel, approvedLabel, filteredLabel, stats, "no content");
-
return;
-
}
-
-
const result = classifyEmail(meta);
-
-
Logger.log(`[${thread.getId()}] Relevant=${result.pertains} Confidence=${result.confidence} Reason="${result.reason}"`);
-
-
if (result.pertains) {
-
applyInboxAction(thread, autoLabel, approvedLabel, filteredLabel, stats, result.reason);
-
} else {
-
applyFilteredAction(thread, autoLabel, filteredLabel, stats, result.reason);
-
}
-
}
-
-
function applyInboxAction(
-
thread: GoogleAppsScript.Gmail.GmailThread,
-
autoLabel: GoogleAppsScript.Gmail.GmailLabel,
-
approvedLabel: GoogleAppsScript.Gmail.GmailLabel,
-
filteredLabel: GoogleAppsScript.Gmail.GmailLabel,
-
stats: any,
-
reason: string
-
): void {
-
if (DRY_RUN) {
-
stats.wouldInbox += 1;
-
Logger.log(` DRY_RUN: Would move to Inbox (${reason})`);
-
} else {
-
thread.removeLabel(autoLabel);
-
thread.removeLabel(filteredLabel);
-
thread.addLabel(approvedLabel);
-
thread.moveToInbox();
-
stats.didInbox += 1;
-
Logger.log(` Applied: Moved to Inbox (${reason})`);
-
}
-
}
-
-
function applyFilteredAction(
-
thread: GoogleAppsScript.Gmail.GmailThread,
-
autoLabel: GoogleAppsScript.Gmail.GmailLabel,
-
filteredLabel: GoogleAppsScript.Gmail.GmailLabel,
-
stats: any,
-
reason: string
-
): void {
-
if (DRY_RUN) {
-
stats.wouldFiltered += 1;
-
Logger.log(` DRY_RUN: Would filter (${reason})`);
-
} else {
-
thread.removeLabel(autoLabel);
-
thread.addLabel(filteredLabel);
-
if (thread.isInInbox()) thread.moveToArchive();
-
stats.didFiltered += 1;
-
Logger.log(` Applied: Filtered (${reason})`);
-
}
-
}
-
-
// Classifier
-
interface ClassificationResult {
-
pertains: boolean;
-
reason: string;
-
confidence: number;
-
}
-
-
interface EmailMeta {
-
subject: string;
-
body: string;
-
from: string;
-
}
-
-
function classifyEmail(meta: EmailMeta): ClassificationResult {
-
const subject = meta.subject.toLowerCase();
-
const body = meta.body.toLowerCase();
-
const combined = subject + " " + body;
-
-
// Security alerts - always relevant
-
const securityResult = checkSecurity(combined);
-
if (securityResult) return securityResult;
-
-
// Student action confirmations
-
const actionResult = checkStudentAction(combined);
-
if (actionResult) return actionResult;
-
-
// Accepted student info
-
const acceptedResult = checkAccepted(combined);
-
if (acceptedResult) return acceptedResult;
-
-
// Dual enrollment
-
const dualResult = checkDualEnrollment(combined);
-
if (dualResult) return dualResult;
-
-
// Scholarships
-
const scholarshipResult = checkScholarship(subject, combined);
-
if (scholarshipResult) return scholarshipResult;
-
-
// Financial aid
-
const aidResult = checkFinancialAid(combined);
-
if (aidResult) return aidResult;
-
-
// Marketing/spam
-
const irrelevantResult = checkIrrelevant(combined);
-
if (irrelevantResult) return irrelevantResult;
-
-
// Default to not relevant
-
return { pertains: false, reason: "No clear relevance indicators", confidence: 0.3 };
-
}
-
-
function checkSecurity(combined: string): ClassificationResult | null {
-
const patterns = [
-
/\bpassword\s+(reset|change|update|expired)\b/,
-
/\breset\s+your\s+password\b/,
-
/\baccount\s+security\b/,
-
/\bsecurity\s+alert\b/,
-
/\bunusual\s+(sign[- ]?in|activity)\b/,
-
/\bverification\s+code\b/,
-
/\b(2fa|mfa|two[- ]factor)\b/,
-
/\bcompromised\s+account\b/,
-
/\baccount\s+(locked|suspended)\b/,
-
/\bsuspicious\s+activity\b/
-
];
-
-
for (let i = 0; i < patterns.length; i++) {
-
if (patterns[i].test(combined)) {
-
if (/\bsaving.*\bon\s+tuition\b|\btuition.*\bsaving\b/.test(combined)) {
-
continue;
-
}
-
return { pertains: true, reason: "Security/password alert", confidence: 1.0 };
-
}
-
}
-
return null;
-
}
-
-
function checkStudentAction(combined: string): ClassificationResult | null {
-
const patterns = [
-
/\bapplication\s+(received|complete|submitted|confirmation)\b/,
-
/\breceived\s+your\s+application\b/,
-
/\bthank\s+you\s+for\s+(applying|submitting)\b/,
-
/\benrollment\s+confirmation\b/,
-
/\bconfirmation\s+(of|for)\s+(your\s+)?(application|enrollment)\b/,
-
/\byour\s+application\s+(has\s+been|is)\s+(received|complete)\b/
-
];
-
-
for (let i = 0; i < patterns.length; i++) {
-
if (patterns[i].test(combined)) {
-
if (/\bhow\s+to\s+apply\b|\bapply\s+now\b|\bstart\s+(your\s+)?application\b/.test(combined)) {
-
continue;
-
}
-
return { pertains: true, reason: "Application/enrollment confirmation", confidence: 0.95 };
-
}
-
}
-
return null;
-
}
-
-
function checkAccepted(combined: string): ClassificationResult | null {
-
const patterns = [
-
/\baccepted\s+(student\s+)?portal\b/,
-
/\byour\s+(personalized\s+)?accepted\s+portal\b/,
-
/\bdeposit\s+(today|now|by|to\s+reserve)\b/,
-
/\breserve\s+your\s+(place|spot)\b/,
-
/\bcongratulations.*\baccepted\b/,
-
/\byou\s+(have\s+been|are|were)\s+accepted\b/,
-
/\badmission\s+(decision|offer)\b/,
-
/\benroll(ment)?\s+deposit\b/
-
];
-
-
for (let i = 0; i < patterns.length; i++) {
-
if (patterns[i].test(combined)) {
-
if (/\bacceptance\s+rate\b|\bhigh\s+acceptance\b|\bpre[- ]admit(ted)?\b|\bautomatic\s+admission\b/.test(combined)) {
-
continue;
-
}
-
if (/\byou\s+will\s+(also\s+)?receive\s+(an?\s+)?(accelerated\s+)?admission\s+decision\b/.test(combined)) {
-
continue;
-
}
-
if (/\breceive\s+an\s+admission\s+decision\s+within\b/.test(combined)) {
-
continue;
-
}
-
return { pertains: true, reason: "Accepted student information", confidence: 0.95 };
-
}
-
}
-
return null;
-
}
-
-
function checkDualEnrollment(combined: string): ClassificationResult | null {
-
const patterns = [
-
/\bdual\s+enrollment\b/,
-
/\bcourse\s+(registration|deletion|added|dropped)\b/,
-
/\bspring\s+\d{4}\s+(course|on[- ]campus)\b/,
-
/\bhow\s+to\s+register\b.*\b(course|class)/
-
];
-
-
for (let i = 0; i < patterns.length; i++) {
-
if (patterns[i].test(combined)) {
-
if (/\blearn\s+more\s+about\b|\binterested\s+in\b|\bconsider\s+joining\b/.test(combined)) {
-
continue;
-
}
-
return { pertains: true, reason: "Dual enrollment course information", confidence: 0.9 };
-
}
-
}
-
return null;
-
}
-
-
function checkScholarship(subject: string, combined: string): ClassificationResult | null {
-
// Specific scholarship applications
-
if (/\bapply\s+for\s+(the\s+)?.*\bscholarship\b/.test(subject)) {
-
if (/\bpresident'?s\b|\bministry\b|\bimpact\b/.test(combined)) {
-
return { pertains: true, reason: "Scholarship application opportunity", confidence: 0.75 };
-
}
-
}
-
-
// Not awarded patterns (check first)
-
const notAwardedPatterns = [
-
/\bscholarship\b.*\b(held|reserved)\s+for\s+you\b/,
-
/\b(held|reserved)\s+for\s+you\b/,
-
/\bconsider(ed|ation)\b.*\bscholarship\b/,
-
/\bscholarship\b.*\bconsider(ed|ation)\b/,
-
/\beligible\s+for\b.*\bscholarship\b/,
-
/\bscholarship\b.*\beligible\b/,
-
/\bmay\s+qualify\b.*\bscholarship\b/
-
];
-
-
if (/\bscholarship\b/.test(combined)) {
-
for (let i = 0; i < notAwardedPatterns.length; i++) {
-
if (notAwardedPatterns[i].test(combined)) {
-
return { pertains: false, reason: "Scholarship not actually awarded", confidence: 0.9 };
-
}
-
}
-
}
-
-
// Awarded patterns
-
const awardedPatterns = [
-
/\bcongratulations\b.*\bscholarship\b/,
-
/\byou\s+(have|received|are\s+awarded|won)\b.*\bscholarship\b/,
-
/\bwe\s+(are\s+)?(pleased\s+to\s+)?award(ing)?\b.*\bscholarship\b/,
-
/\bscholarship\s+(offer|award)\b/
-
];
-
-
for (let i = 0; i < awardedPatterns.length; i++) {
-
if (awardedPatterns[i].test(combined)) {
-
return { pertains: true, reason: "Scholarship awarded", confidence: 0.95 };
-
}
-
}
-
-
return null;
-
}
-
-
function checkFinancialAid(combined: string): ClassificationResult | null {
-
const readyPatterns = [
-
/\bfinancial\s+aid\b.*\boffer\b.*\b(ready|available)\b/,
-
/\baward\s+letter\b.*\b(ready|available|posted|view)\b/,
-
/\b(view|review)\s+(your\s+)?award\s+letter\b/,
-
/\byour\s+aid\s+is\s+ready\b/
-
];
-
-
const notReadyPatterns = [
-
/\blearn\s+more\s+about\b.*\bfinancial\s+aid\b/,
-
/\bapply\b.*\b(for\s+)?financial\s+aid\b/,
-
/\bcomplete\s+(your\s+)?fafsa\b/,
-
/\bpriority\s+(deadline|consideration)\b.*\bfinancial\s+aid\b/
-
];
-
-
for (let i = 0; i < readyPatterns.length; i++) {
-
if (readyPatterns[i].test(combined)) {
-
for (let j = 0; j < notReadyPatterns.length; j++) {
-
if (notReadyPatterns[j].test(combined)) {
-
return null;
-
}
-
}
-
return { pertains: true, reason: "Financial aid offer ready", confidence: 0.95 };
-
}
-
}
-
return null;
-
}
-
-
function checkIrrelevant(combined: string): ClassificationResult | null {
-
const patterns = [
-
/\bstudent\s+life\s+blog\b/,
-
/\bnewsletter\b/,
-
/\bweekly\s+(digest|update)\b/,
-
/\bupcoming\s+events\b/,
-
/\bjoin\s+us\s+(for|at)\b/,
-
/\bopen\s+house\b/,
-
/\bvirtual\s+tour\b/,
-
/\bhaven'?t\s+applied.*yet\b/,
-
/\bstill\s+time\s+to\s+apply\b/,
-
/\bhow\s+is\s+your\s+college\s+search\b/,
-
/\bextended.*\bpriority\s+deadline\b/,
-
/\bpriority\s+deadline.*\bextended\b/,
-
/\bsummer\s+(academy|camp|program)\b/,
-
/\bugly\s+sweater\b/,
-
/\bi\s+hope\s+you\s+have\s+been\s+receiving\s+my\s+emails\b/,
-
/\bam\s+i\s+reaching\b/,
-
/\byou\s+are\s+on\s+.*\s+(radar|list)\b/,
-
/\bi\s+want\s+to\s+make\s+sure\s+you\s+know\b/,
-
/\byou'?re\s+invited\s+to\s+submit\b/,
-
/\bi'?m\s+eager\s+to\s+consider\s+you\b/,
-
/\bsubmit\s+your\s+.*\s+application\b/,
-
/\bpriority\s+status\b.*\bsubmit.*application\b/
-
];
-
-
for (let i = 0; i < patterns.length; i++) {
-
if (patterns[i].test(combined)) {
-
return { pertains: false, reason: "Marketing/newsletter/spam", confidence: 0.95 };
-
}
-
}
-
-
if (/\bhaven'?t\s+applied\b/.test(combined)) {
-
return { pertains: false, reason: "Unsolicited outreach", confidence: 0.95 };
-
}
-
-
return null;
-
}
-
-
// Utilities
-
function getOrCreateLabel(name: string): GoogleAppsScript.Gmail.GmailLabel {
-
return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
-
}
-
-
function safeStr(s: string | null, maxLen?: number): string {
-
if (s === null || s === undefined) return "";
-
const str = s.toString().trim();
-
if (maxLen && str.length > maxLen) return str.slice(0, maxLen);
-
return str;
-
}
-
-
function setupTriggers(): void {
-
// Delete existing triggers
-
const triggers = ScriptApp.getProjectTriggers();
-
for (let i = 0; i < triggers.length; i++) {
-
if (triggers[i].getHandlerFunction() === "runTriage") {
-
ScriptApp.deleteTrigger(triggers[i]);
-
}
-
}
-
-
// Create new trigger
-
ScriptApp.newTrigger("runTriage")
-
.timeBased()
-
.everyMinutes(10)
-
.create();
-
-
Logger.log("Trigger created: runTriage every 10 minutes");
-
}
-
+
// DEPRECATED: This file is no longer used.
+
//
+
// The Apps Script is now built from wrapper.ts which imports the main classifier.ts
+
// This ensures the Apps Script always uses the same logic as the TypeScript version.
+
//
+
// To build: bun run gas
+
// Source: src/apps-script/wrapper.ts + src/classifier.ts
+
//
+
// This file is kept for reference only.
+196
src/apps-script/wrapper.ts
···
+
// Apps Script wrapper - imports the main classifier
+
// This file is bundled into a single .gs file
+
+
import { classifyEmail } from "../classifier";
+
import type { EmailInput, ClassificationResult } from "../types";
+
+
// Configuration
+
const AUTO_LABEL_NAME = "College/Auto";
+
const FILTERED_LABEL_NAME = "College/Filtered";
+
const APPROVED_LABEL_NAME = "College";
+
const DRY_RUN = true;
+
+
const MAX_THREADS_PER_RUN = 75;
+
const MAX_EXECUTION_TIME_MS = 4.5 * 60 * 1000;
+
const GMAIL_BATCH_SIZE = 20;
+
+
// Declare global for Apps Script
+
declare const GmailApp: any;
+
declare const Logger: any;
+
declare const Utilities: any;
+
declare const ScriptApp: any;
+
+
// Main entry points
+
function ensureLabels(): void {
+
getOrCreateLabel(AUTO_LABEL_NAME);
+
getOrCreateLabel(FILTERED_LABEL_NAME);
+
getOrCreateLabel(APPROVED_LABEL_NAME);
+
Logger.log(`Labels ensured: ${AUTO_LABEL_NAME}, ${FILTERED_LABEL_NAME}, ${APPROVED_LABEL_NAME}`);
+
}
+
+
function runTriage(): void {
+
const startTime = Date.now();
+
const autoLabel = getOrCreateLabel(AUTO_LABEL_NAME);
+
const filteredLabel = getOrCreateLabel(FILTERED_LABEL_NAME);
+
const approvedLabel = getOrCreateLabel(APPROVED_LABEL_NAME);
+
+
const threads = autoLabel.getThreads(0, MAX_THREADS_PER_RUN);
+
if (!threads.length) {
+
Logger.log("No threads under College/Auto.");
+
return;
+
}
+
+
Logger.log(`Processing ${threads.length} threads`);
+
+
let stats = {
+
wouldInbox: 0,
+
wouldFiltered: 0,
+
didInbox: 0,
+
didFiltered: 0,
+
errors: 0,
+
skipped: 0
+
};
+
+
for (let i = 0; i < threads.length; i++) {
+
const elapsed = Date.now() - startTime;
+
if (elapsed > MAX_EXECUTION_TIME_MS) {
+
Logger.log(`Time limit reached. Processed ${i}/${threads.length}`);
+
stats.skipped = threads.length - i;
+
break;
+
}
+
+
const thread = threads[i];
+
+
try {
+
processThread(thread, autoLabel, approvedLabel, filteredLabel, stats);
+
} catch (e) {
+
Logger.log(`ERROR: ${e}. FAIL-SAFE: Moving to inbox.`);
+
stats.errors += 1;
+
+
if (!DRY_RUN) {
+
thread.removeLabel(autoLabel);
+
thread.removeLabel(filteredLabel);
+
thread.moveToInbox();
+
stats.didInbox += 1;
+
} else {
+
stats.wouldInbox += 1;
+
}
+
}
+
+
if ((i + 1) % GMAIL_BATCH_SIZE === 0) {
+
Utilities.sleep(100);
+
}
+
}
+
+
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
+
Logger.log(`Summary: Inbox=${stats.wouldInbox}/${stats.didInbox}, Filtered=${stats.wouldFiltered}/${stats.didFiltered}, Errors=${stats.errors}, Time=${totalTime}s`);
+
}
+
+
function processThread(
+
thread: any,
+
autoLabel: any,
+
approvedLabel: any,
+
filteredLabel: any,
+
stats: any
+
): void {
+
const msg = thread.getMessages()[thread.getMessages().length - 1];
+
if (!msg) throw new Error("No messages in thread");
+
+
const meta: EmailInput = {
+
subject: safeStr(msg.getSubject()),
+
body: safeStr(msg.getPlainBody(), 10000),
+
from: safeStr(msg.getFrom()),
+
};
+
+
if (!meta.subject && !meta.body) {
+
Logger.log(`WARNING: No content. FAIL-SAFE: Moving to inbox.`);
+
applyInboxAction(thread, autoLabel, approvedLabel, filteredLabel, stats, "no content");
+
return;
+
}
+
+
const result = classifyEmail(meta);
+
+
Logger.log(`[${thread.getId()}] Relevant=${result.pertains} Confidence=${result.confidence} Reason="${result.reason}"`);
+
+
if (result.pertains) {
+
applyInboxAction(thread, autoLabel, approvedLabel, filteredLabel, stats, result.reason);
+
} else {
+
applyFilteredAction(thread, autoLabel, filteredLabel, stats, result.reason);
+
}
+
}
+
+
function applyInboxAction(
+
thread: any,
+
autoLabel: any,
+
approvedLabel: any,
+
filteredLabel: any,
+
stats: any,
+
reason: string
+
): void {
+
if (DRY_RUN) {
+
stats.wouldInbox += 1;
+
Logger.log(` DRY_RUN: Would move to Inbox (${reason})`);
+
} else {
+
thread.removeLabel(autoLabel);
+
thread.removeLabel(filteredLabel);
+
thread.addLabel(approvedLabel);
+
thread.moveToInbox();
+
stats.didInbox += 1;
+
Logger.log(` Applied: Moved to Inbox (${reason})`);
+
}
+
}
+
+
function applyFilteredAction(
+
thread: any,
+
autoLabel: any,
+
filteredLabel: any,
+
stats: any,
+
reason: string
+
): void {
+
if (DRY_RUN) {
+
stats.wouldFiltered += 1;
+
Logger.log(` DRY_RUN: Would filter (${reason})`);
+
} else {
+
thread.removeLabel(autoLabel);
+
thread.addLabel(filteredLabel);
+
if (thread.isInInbox()) thread.moveToArchive();
+
stats.didFiltered += 1;
+
Logger.log(` Applied: Filtered (${reason})`);
+
}
+
}
+
+
// Utilities
+
function getOrCreateLabel(name: string): any {
+
return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
+
}
+
+
function safeStr(s: string | null, maxLen?: number): string {
+
if (s === null || s === undefined) return "";
+
const str = s.toString().trim();
+
if (maxLen && str.length > maxLen) return str.slice(0, maxLen);
+
return str;
+
}
+
+
function setupTriggers(): void {
+
// Delete existing triggers
+
const triggers = ScriptApp.getProjectTriggers();
+
for (let i = 0; i < triggers.length; i++) {
+
if (triggers[i].getHandlerFunction() === "runTriage") {
+
ScriptApp.deleteTrigger(triggers[i]);
+
}
+
}
+
+
// Create new trigger
+
ScriptApp.newTrigger("runTriage")
+
.timeBased()
+
.everyMinutes(10)
+
.create();
+
+
Logger.log("Trigger created: runTriage every 10 minutes");
+
}
+
+
// Export for Apps Script global scope - these become top-level functions
+
// Note: The bundler needs to be configured to expose these properly
+
+
// Make sure functions are not tree-shaken by referencing them
+
export { ensureLabels, runTriage, setupTriggers };
+28 -15
src/build-gas.ts
···
#!/usr/bin/env bun
// Build script for Google Apps Script
-
import { execSync } from "child_process";
+
import * as esbuild from "esbuild";
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
const command = process.argv[2] || "build";
if (command === "build") {
-
console.log("🔨 Building for Apps Script (local .gs file)...\n");
+
console.log("🔨 Building for Apps Script (bundled from main classifier)...\n");
-
// Compile TypeScript
-
console.log("1️⃣ Compiling TypeScript...");
+
// Bundle with esbuild
+
console.log("1️⃣ Bundling with esbuild...");
try {
-
execSync("tsc -p tsconfig.apps-script.json", { stdio: "inherit" });
-
console.log("✅ TypeScript compiled\n");
+
await esbuild.build({
+
entryPoints: ["src/apps-script/wrapper.ts"],
+
bundle: true,
+
outfile: "build/bundled.js",
+
format: "esm", // Use ES modules format
+
target: "es2015",
+
platform: "neutral",
+
});
+
console.log("✅ Bundled successfully\n");
} catch (e) {
-
console.error("❌ TypeScript compilation failed");
+
console.error("❌ Bundling failed:", e);
process.exit(1);
}
-
// Create .gs file
-
console.log("2️⃣ Creating .gs file...");
-
const { mkdirSync, readFileSync, writeFileSync } = await import("fs");
+
// Post-process: Convert to Apps Script compatible format
+
console.log("2️⃣ Post-processing for Apps Script...");
mkdirSync("build", { recursive: true });
-
const js = readFileSync("build/compiled/Code.js", "utf-8");
+
let js = readFileSync("build/bundled.js", "utf-8");
+
+
// Remove export statements - functions will be top-level
+
js = js.replace(/^export \{[^}]+\};?\s*$/gm, '');
+
js = js.replace(/^export (function|const|var|let) /gm, '$1 ');
-
const header = `// Auto-generated from TypeScript at: ${new Date().toISOString()}
-
// Source: src/apps-script/Code.ts
+
const output = `// Auto-generated from TypeScript at: ${new Date().toISOString()}
+
// Source: src/apps-script/wrapper.ts + src/classifier.ts
// DO NOT EDIT THIS FILE DIRECTLY - Edit the TypeScript source instead
+
// This file is bundled from the main classifier to ensure consistency
+
${js}
`;
-
writeFileSync("build/Code.gs", header + js);
+
writeFileSync("build/Code.gs", output);
console.log("✅ Created build/Code.gs\n");
console.log("🎉 Build complete!");
···
} else {
console.log("Usage: bun run gas [build]");
console.log("\nCommands:");
-
console.log(" build - Compile TypeScript to .gs file (default)");
+
console.log(" build - Bundle TypeScript to .gs file (default)");
}
+18 -1
src/classifier.ts
···
if (/\breceive\s+an\s+admission\s+decision\s+within\b/.test(combined)) {
return null;
}
+
// Exclude "Priority Student" spam that asks to submit application
+
if (/\bpriority\s+student\b.*\bsubmit.*application\b|\bsubmit.*\bpriority\s+student\s+application\b/.test(combined)) {
+
return null;
+
}
+
// Exclude if asking to submit ANY application (not accepted yet)
+
if (/\bif\s+you\s+haven'?t\s+yet\s+done\s+so.*submit\b|\bsubmit\s+the\b.*\bapplication\b/.test(combined)) {
+
return null;
+
}
+
// Exclude "reserve your spot" for events/webinars (not enrollment)
+
if (/\breserve\s+your\s+spot\b/.test(combined) && /\b(virtual|webinar|event|program|zoom|session)\b/.test(combined)) {
+
return null;
+
}
return {
pertains: true,
reason: "Accepted student portal/deposit information",
···
// Marketing events
/\bupcoming\s+events\b/,
-
/\bjoin\s+us\s+(for|at)\b/,
+
/\bjoin\s+us\s+(for|at|on\s+zoom)\b/,
/\bopen\s+house\b/,
/\bvirtual\s+tour\b/,
/\bcampus\s+(visit|tour|event)\b/,
···
// Ugly sweaters and other fluff
/\bugly\s+sweater\b/,
/\bit'?s\s+.+\s+season\b/,
+
+
// FAFSA/scholarship info sessions (not actual aid offers)
+
/\bjoin\s+us.*\b(virtual\s+program|zoom)\b.*\b(scholarship|financial\s+aid)\b/,
+
/\blearn\s+more\b.*\b(scholarship|financial\s+aid)\s+(opportunities|options)\b/,
+
/\b(scholarship|financial\s+aid)\s+(opportunities|options)\b.*\blearn\s+more\b/,
];
for (const pattern of irrelevantPatterns) {