227 lines
8.0 KiB
JavaScript
227 lines
8.0 KiB
JavaScript
/***************
|
|
* CONFIG
|
|
***************/
|
|
const CONFIG = {
|
|
SPREADSHEET_ID: "17II0V8EsTL-9nHq2HgqhHcay-t-hhOaY5wNEzXHEsAM",
|
|
SOURCE_SHEET_NAME: "WSMinMCA.csv", // tab name inside the spreadsheet
|
|
EMAIL_COLUMN_INDEX_1BASED: 3, // Column C = 3 (1-based)
|
|
SENDER: "nrasmussen@gainsight.com",
|
|
SUBJECT: "DATA REQUEST",
|
|
PROCESSED_LABEL: "DATA_REQUEST_PROCESSED",
|
|
TEMP_SHEET_PREFIX: "DATA_REQUEST_", // new tab prefix
|
|
MAX_THREADS_PER_RUN: 10 // safety throttle
|
|
};
|
|
|
|
/**
|
|
* Run once to create the time-driven trigger.
|
|
* (There is no true "email received" trigger for Gmail in Apps Script.)
|
|
*/
|
|
function setupTrigger() {
|
|
// Optional: delete existing triggers for this function to avoid duplicates
|
|
ScriptApp.getProjectTriggers()
|
|
.filter(t => t.getHandlerFunction() === "processDataRequests")
|
|
.forEach(t => ScriptApp.deleteTrigger(t));
|
|
|
|
// Create a trigger every 5 minutes (adjust as desired)
|
|
ScriptApp.newTrigger("processDataRequests")
|
|
.timeBased()
|
|
.everyMinutes(5)
|
|
.create();
|
|
}
|
|
|
|
/**
|
|
* Main worker: finds unread "DATA REQUEST" messages from aubrey@ws.com,
|
|
* extracts target email from body, filters rows, creates new tab, CSVs it,
|
|
* replies with attachment, and labels thread as processed.
|
|
*/
|
|
function processDataRequests() {
|
|
// const label = GmailApp.getUserLabelByName(CONFIG.PROCESSED_LABEL) || GmailApp.createLabel(CONFIG.PROCESSED_LABEL);
|
|
|
|
// // Search unread matching threads that are not already labeled processed
|
|
// const query = [
|
|
// `from:${CONFIG.SENDER}`,
|
|
// `subject:"${CONFIG.SUBJECT}"`,
|
|
// "is:unread",
|
|
// `-label:${CONFIG.PROCESSED_LABEL}`
|
|
// ].join(" ");
|
|
|
|
const threads = GmailApp.search(query, 0, CONFIG.MAX_THREADS_PER_RUN);
|
|
Logger.log(threads);
|
|
// if (!threads.length) return;
|
|
|
|
const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
|
|
const sourceSheet = ss.getSheetByName(CONFIG.SOURCE_SHEET_NAME);
|
|
if (!sourceSheet) throw new Error(`Sheet tab not found: ${CONFIG.SOURCE_SHEET_NAME}`);
|
|
|
|
// Read all data once for efficiency
|
|
const sourceValues = sourceSheet.getDataRange().getValues();
|
|
if (sourceValues.length < 1) throw new Error(`No data found in ${CONFIG.SOURCE_SHEET_NAME}`);
|
|
|
|
const header = sourceValues[0];
|
|
const emailColIdx0 = CONFIG.EMAIL_COLUMN_INDEX_1BASED - 1;
|
|
|
|
threads.forEach(thread => {
|
|
const messages = thread.getMessages();
|
|
|
|
// Process only unread messages in the thread (usually just the newest)
|
|
messages.forEach(message => {
|
|
if (!message.isUnread()) return;
|
|
|
|
try {
|
|
const plainBody = message.getPlainBody() || "";
|
|
const targetEmail = extractFirstNonSenderEmail_(plainBody, CONFIG.SENDER);
|
|
|
|
if (!targetEmail) {
|
|
message.reply(
|
|
`I received your DATA REQUEST, but I couldn't find a target email address in the message body.\n\n` +
|
|
`Please include an email like "someone@company.com" in the body and resend.`
|
|
);
|
|
message.markRead();
|
|
return;
|
|
}
|
|
|
|
const targetLower = targetEmail.toLowerCase();
|
|
|
|
// Filter rows where Column C exactly matches the extracted email (case-insensitive)
|
|
const matches = sourceValues
|
|
.slice(1) // skip header
|
|
.filter(row => String(row[emailColIdx0] || "").trim().toLowerCase() === targetLower);
|
|
|
|
// Create a new tab with results
|
|
const newSheetName = makeSafeSheetName_(
|
|
`${CONFIG.TEMP_SHEET_PREFIX}${targetLower}_${Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyyMMdd_HHmmss")}`
|
|
);
|
|
|
|
const resultSheet = ss.insertSheet(newSheetName);
|
|
resultSheet.getRange(1, 1, 1, header.length).setValues([header]);
|
|
|
|
if (matches.length) {
|
|
resultSheet.getRange(2, 1, matches.length, header.length).setValues(matches);
|
|
}
|
|
|
|
// Convert result tab to CSV
|
|
const csv = valuesToCsv_(resultSheet.getDataRange().getValues());
|
|
const blob = Utilities.newBlob(csv, "text/csv", `${targetLower}.csv`);
|
|
|
|
// Reply with CSV attached (keeps threading)
|
|
const body =
|
|
`Attached is the CSV export for ${targetEmail}.\n\n` +
|
|
`Rows matched (excluding header): ${matches.length}\n` +
|
|
`Source tab: ${CONFIG.SOURCE_SHEET_NAME}\n` +
|
|
`Result tab: ${newSheetName}\n`;
|
|
|
|
message.reply(body, { attachments: [blob] });
|
|
|
|
// Mark processed
|
|
message.markRead();
|
|
thread.addLabel(label);
|
|
|
|
// Optional cleanup: uncomment if you do NOT want to keep the result tab
|
|
// ss.deleteSheet(resultSheet);
|
|
|
|
} catch (err) {
|
|
// Fail gracefully: reply with error so the sender knows it didn't work
|
|
message.reply(
|
|
`I attempted to process this DATA REQUEST but hit an error:\n\n${err && err.stack ? err.stack : err}`
|
|
);
|
|
message.markRead();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function modifiedProcessDataRequests() {
|
|
const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
|
|
const sourceSheet = ss.getSheetByName(CONFIG.SOURCE_SHEET_NAME);
|
|
if (!sourceSheet) throw new Error(`Sheet tab not found: ${CONFIG.SOURCE_SHEET_NAME}`);
|
|
const targetEmail = "khodge2@wsgc.com";
|
|
const targetLower = targetEmail.toLowerCase();
|
|
|
|
// Read all data once for efficiency
|
|
const sourceValues = sourceSheet.getDataRange().getValues();
|
|
if (sourceValues.length < 1) throw new Error(`No data found in ${CONFIG.SOURCE_SHEET_NAME}`);
|
|
|
|
const header = sourceValues[0];
|
|
Logger.log(header);
|
|
const emailColIdx0 = CONFIG.EMAIL_COLUMN_INDEX_1BASED - 1;
|
|
Logger.log(emailColIdx0);
|
|
|
|
// Filter rows where Column C exactly matches the extracted email (case-insensitive)
|
|
const matches = sourceValues
|
|
.slice(1) // skip header
|
|
.filter(row => String(row[emailColIdx0] || "").trim().toLowerCase() === targetLower);
|
|
|
|
// Create a new tab with results
|
|
const newSheetName = makeSafeSheetName_(
|
|
`${CONFIG.TEMP_SHEET_PREFIX}${targetLower}_${Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyyMMdd_HHmmss")}`
|
|
);
|
|
|
|
const resultSheet = ss.insertSheet(newSheetName);
|
|
resultSheet.getRange(1, 1, 1, header.length).setValues([header]);
|
|
|
|
if (matches.length) {
|
|
resultSheet.getRange(2, 1, matches.length, header.length).setValues(matches);
|
|
}
|
|
|
|
// Convert result tab to CSV
|
|
const csv = valuesToCsv_(resultSheet.getDataRange().getValues());
|
|
const blob = Utilities.newBlob(csv, "text/csv", `${targetLower}.csv`);
|
|
|
|
// Reply with CSV attached (keeps threading)
|
|
const body =
|
|
`Attached is the CSV export for ${targetEmail}.\n\n` +
|
|
`Rows matched (excluding header): ${matches.length}\n` +
|
|
`Source tab: ${CONFIG.SOURCE_SHEET_NAME}\n` +
|
|
`Result tab: ${newSheetName}\n`;
|
|
|
|
Logger.log(body);
|
|
|
|
// Optional cleanup: uncomment if you do NOT want to keep the result tab
|
|
// ss.deleteSheet(resultSheet);
|
|
}
|
|
|
|
/****************
|
|
* HELPERS
|
|
****************/
|
|
|
|
/**
|
|
* Extracts the first email address found in the text that is NOT the sender email.
|
|
*/
|
|
function extractFirstNonSenderEmail_(text, senderEmail) {
|
|
const re = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig;
|
|
const found = text.match(re) || [];
|
|
const senderLower = (senderEmail || "").toLowerCase();
|
|
const first = found
|
|
.map(e => e.trim())
|
|
.find(e => e.toLowerCase() !== senderLower);
|
|
return first || null;
|
|
}
|
|
|
|
/**
|
|
* Converts a 2D array of values to RFC-ish CSV:
|
|
* - Wrap fields in quotes if they contain comma, quote, or newline
|
|
* - Double internal quotes
|
|
*/
|
|
function valuesToCsv_(values) {
|
|
return values.map(row => row.map(cell => {
|
|
let s = cell === null || cell === undefined ? "" : String(cell);
|
|
// Normalize line endings inside cells
|
|
s = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
const needsQuotes = /[",\n]/.test(s);
|
|
if (needsQuotes) s = `"${s.replace(/"/g, '""')}"`;
|
|
return s;
|
|
}).join(",")).join("\r\n");
|
|
}
|
|
|
|
/**
|
|
* Google Sheets tab names have limits; keep it safe.
|
|
*/
|
|
function makeSafeSheetName_(name) {
|
|
// Remove characters Sheets doesn't like: [ ] : * ? / \
|
|
let safe = name.replace(/[\[\]:*?/\\]/g, " ");
|
|
safe = safe.replace(/\s+/g, " ").trim();
|
|
// Max length 100
|
|
if (safe.length > 100) safe = safe.slice(0, 100);
|
|
return safe || "DATA_REQUEST_RESULT";
|
|
}
|