Gmail folder auto-advance + /forward command
- Reply-cycle auto-advance: staff reply files the thread to "Awaiting Reply", a customer response files it to "Needs Response" (new GMAIL_LABEL_AWAITING_REPLY / GMAIL_LABEL_NEEDS_RESPONSE labels + autoAdvanceFolder, which only moves threads still in the auto-cycle and leaves hand-filed folders alone) - /forward: forward a ticket's email to another address (handlers/commands/forward.js + forward composition in services/gmail.js) - Tests for the auto-advance cycle; label fixtures updated for the new labels
This commit is contained in:
@@ -19,6 +19,8 @@ const { getGmailClient } = require('./gmail');
|
||||
// name from CONFIG (env-configurable); SPAM is the Gmail system label.
|
||||
const FOLDER_DEFS = {
|
||||
TRIAGE: { configKey: 'GMAIL_LABEL_TRIAGE' },
|
||||
AWAITING_REPLY: { configKey: 'GMAIL_LABEL_AWAITING_REPLY' },
|
||||
NEEDS_RESPONSE: { configKey: 'GMAIL_LABEL_NEEDS_RESPONSE' },
|
||||
ESCALATED: { configKey: 'GMAIL_LABEL_ESCALATED' },
|
||||
RESOLVED: { configKey: 'GMAIL_LABEL_RESOLVED' },
|
||||
FOR_JAKE: { configKey: 'GMAIL_LABEL_FOR_JAKE' },
|
||||
@@ -30,6 +32,12 @@ const FOLDER_DEFS = {
|
||||
// User-managed folder keys (everything but the system SPAM label).
|
||||
const MANAGED_USER_KEYS = Object.keys(FOLDER_DEFS).filter(k => !FOLDER_DEFS[k].system);
|
||||
|
||||
// Folders staff file into by hand. The reply-cycle auto-flow (autoAdvanceFolder)
|
||||
// never moves a thread out of one of these — a customer reply to something filed
|
||||
// under "For Jake" or "Spam" stays put. Everything else (Triage, Awaiting Reply,
|
||||
// Needs Response, Escalated, Resolved) is auto-cycle eligible.
|
||||
const MANUAL_KEYS = ['FOR_JAKE', 'SPAM', 'PARTNERSHIP_OFFERS', 'DASHBOARD_ERRORS'];
|
||||
|
||||
// Always stripped on a move so the thread leaves the inbox and is marked read.
|
||||
const ALWAYS_REMOVE = ['INBOX', 'UNREAD'];
|
||||
|
||||
@@ -139,14 +147,60 @@ async function moveThreadToFolder(threadId, targetKey, gmail = getGmailClient())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only: which managed folder does this thread currently sit in? Returns a
|
||||
* FOLDER_DEFS key, or null if none of the managed labels are present.
|
||||
*
|
||||
* Unlike resolveLabelId this never *creates* a label — a label that doesn't exist
|
||||
* yet can't be on the thread, so it's simply skipped.
|
||||
*/
|
||||
async function getManagedFolderKey(threadId, gmail = getGmailClient()) {
|
||||
if (!threadId) throw new Error('getManagedFolderKey: threadId required');
|
||||
|
||||
const thread = await gmail.users.threads.get({ userId: 'me', id: threadId, format: 'minimal' });
|
||||
const threadLabelIds = new Set();
|
||||
for (const m of thread.data.messages || []) {
|
||||
for (const id of m.labelIds || []) threadLabelIds.add(id);
|
||||
}
|
||||
|
||||
await ensureLabelCache(gmail);
|
||||
for (const key of Object.keys(FOLDER_DEFS)) {
|
||||
const def = FOLDER_DEFS[key];
|
||||
const id = def.system ? def.system : labelIdByName.get(folderDisplayName(key));
|
||||
if (id && threadLabelIds.has(id)) return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance a thread to `targetKey` as part of the reply cycle — UNLESS it is
|
||||
* currently filed in a manual folder (MANUAL_KEYS), in which case it is left
|
||||
* untouched. Threads with no managed label (or an auto-cycle label) advance.
|
||||
*
|
||||
* @returns {Promise<boolean>} true if the thread was moved, false if left in place.
|
||||
*/
|
||||
async function autoAdvanceFolder(threadId, targetKey, gmail = getGmailClient()) {
|
||||
if (!threadId) throw new Error('autoAdvanceFolder: threadId required');
|
||||
if (!FOLDER_DEFS[targetKey]) throw new Error(`Unknown folder key: ${targetKey}`);
|
||||
|
||||
const current = await getManagedFolderKey(threadId, gmail);
|
||||
if (current && MANUAL_KEYS.includes(current)) return false;
|
||||
|
||||
await moveThreadToFolder(threadId, targetKey, gmail);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FOLDER_DEFS,
|
||||
MANAGED_USER_KEYS,
|
||||
MANUAL_KEYS,
|
||||
ALWAYS_REMOVE,
|
||||
folderDisplayName,
|
||||
resolveLabelId,
|
||||
computeLabelMutation,
|
||||
moveThreadToFolder,
|
||||
getManagedFolderKey,
|
||||
autoAdvanceFolder,
|
||||
// test seam: clear the name->id cache between cases
|
||||
__clearLabelCache: () => labelIdByName.clear()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user