/** * 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; }