From e1f92ea7c676c55fb1de1eb073553ff4b1f68a28 Mon Sep 17 00:00:00 2001 From: mpmedia Date: Sun, 7 Jun 2026 10:46:02 -0500 Subject: [PATCH] Add example: DocScan Apps Script reference implementation --- examples/docscan_appscript_example.gs | 119 ++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 examples/docscan_appscript_example.gs diff --git a/examples/docscan_appscript_example.gs b/examples/docscan_appscript_example.gs new file mode 100644 index 0000000..433db73 --- /dev/null +++ b/examples/docscan_appscript_example.gs @@ -0,0 +1,119 @@ +/** + * Gmail Inbox Architect — DocScan Apps Script + * Reference implementation for attachment-based Gmail classification. + * + * Deploy in Google Apps Script (script.google.com). Run once manually + * to authorize Gmail scope, then set a time-driven trigger (hourly recommended). + * + * BEFORE RUNNING: Create these labels in Gmail if they don't exist: + * - Documentation + * - Z-Archive/DocScan (set to hidden in Gmail label settings) + * + * HOW IT WORKS: + * 1. Searches Gmail for recent threads with attachments not yet scanned + * 2. Inspects each attachment filename and MIME type + * 3. Applies "Documentation" label if attachment matches known patterns + * 4. Applies "Z-Archive/DocScan" guard label to ALL scanned threads + * (prevents re-scanning — this label appears even on non-matching threads) + * + * EXTENDING: Add new detection patterns by adding `if` branches in the + * attachment loop. See examples below. + */ + +function scanDocumentationAttachments() { + const DOC_LABEL = getLabel_("Documentation"); // your target label + const SCAN_LABEL = getLabel_("Z-Archive/DocScan"); // hidden guard label + + // Search criteria: recent, has attachment, not already scanned, not trash/spam + const query = [ + "newer_than:14d", + "has:attachment", + "-in:trash", + "-in:spam", + '-label:"Z-Archive/DocScan"' + ].join(" "); + + const threads = GmailApp.search(query, 0, 50); // max 50 threads per run + + for (const thread of threads) { + let applyDoc = false; + const reasons = []; + + for (const message of thread.getMessages()) { + // Build text context for keyword matching (subject + body) + const text = [ + message.getSubject() || "", + message.getPlainBody ? message.getPlainBody() : "" + ].join("\n"); + + const hasDiagram = /\bdiagram\b/i.test(text); + + for (const att of message.getAttachments({ + includeInlineImages: false, + includeAttachments: true + })) { + const name = (att.getName() || "").toLowerCase(); + const mime = (att.getContentType() || "").toLowerCase(); + + // --- Detection patterns --- add more here as needed --- + + // Engineering STEP/STP files (CAD geometry) + if (/\.(stp|step)$/.test(name)) { + applyDoc = true; + reasons.push(`STEP file: ${name}`); + } + + // PDF attachments when email body/subject mentions "diagram" + if (hasDiagram && (/\.pdf$/.test(name) || mime === "application/pdf")) { + applyDoc = true; + reasons.push(`diagram+PDF: ${name}`); + } + + // --- Additional pattern examples (uncomment to enable) --- + + // Vendor invoices by filename pattern + // if (/inv[-_]\d+\.pdf$/i.test(name)) { + // thread.addLabel(getLabel_("Finance/Invoices-Vendor-Payable")); + // reasons.push(`vendor invoice: ${name}`); + // } + + // Purchase orders by filename pattern + // if (/\b(po|purchase.order)\b.*\.pdf$/i.test(name)) { + // thread.addLabel(getLabel_("Finance/Purchase-Orders")); + // reasons.push(`purchase order: ${name}`); + // } + + // Signed contracts + // if (/(signed|executed).*\.pdf$/i.test(name)) { + // applyDoc = true; + // reasons.push(`signed contract: ${name}`); + // } + } + } + + // Apply Documentation label if any pattern matched + if (applyDoc) { + thread.addLabel(DOC_LABEL); + console.log( + `Labeled Documentation — "${thread.getFirstMessageSubject()}" | ${reasons.join("; ")}` + ); + } + + // Always apply guard label (marks thread as scanned regardless of match) + thread.addLabel(SCAN_LABEL); + } +} + +/** + * Helper: get a Gmail label by name, throw a clear error if it doesn't exist. + * Create labels in Gmail before running this script — the script will not create them. + */ +function getLabel_(name) { + const label = GmailApp.getUserLabelByName(name); + if (!label) { + throw new Error( + `Label not found: "${name}" — create it in Gmail settings before running this script.` + ); + } + return label; +}