Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/gmail-to-drive-by-labels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,25 @@ function getProcessConfig() {

## Usage

### Regular Processing

1. Select `storeEmailsAndAttachments` from the function dropdown in the Apps Script toolbar.
2. Click **Run**.
3. Grant permissions when prompted (access to Gmail, Drive, and Docs).
4. Check the **Execution Log** for progress.

### Rebuilding Documents

If you've updated the cleaning logic (e.g., `getCleanBody` function) or want to regenerate documents with new processing rules:

1. Select `rebuildAllDocs` from the function dropdown in the Apps Script toolbar.
2. Click **Run** - this will:
* Clear all configured Google Docs
* Move all processed/archived emails back to their trigger labels
3. Then run `storeEmailsAndAttachments` to reprocess all emails with the updated logic.

**Note:** The rebuild process moves (not copies) emails back to trigger labels, ensuring all emails are reprocessed exactly once with the latest logic while maintaining incremental processing to avoid script timeouts.

## Automation (Optional)

To run this script automatically (e.g., every hour):
Expand Down
166 changes: 166 additions & 0 deletions src/gmail-to-drive-by-labels/code.gs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,164 @@ function storeEmailsAndAttachments() {
console.log('[storeEmailsAndAttachments] Completed all processing');
}

/**
* Rebuilds all configured documents by clearing them and reprocessing all emails.
* This function:
* 1. Clears the configured Google Doc
* 2. Moves all processed/archived emails back to the trigger label
* 3. Allows storeEmailsAndAttachments() to reprocess them
*
* For large label sets, this function uses batching to avoid timeouts.
* If interrupted, run again to continue from where it left off.
*
* Run this when you've updated getCleanBody() or other processing logic
* and want to regenerate the documents with the new logic.
*/
function rebuildAllDocs() {
console.log('[rebuildAllDocs] Starting rebuild process');
var PROCESS_CONFIG = getProcessConfig();
console.log('[rebuildAllDocs] Rebuilding', PROCESS_CONFIG.length, 'configurations');

var completed = true;
for (var i = 0; i < PROCESS_CONFIG.length; i++) {
var config = PROCESS_CONFIG[i];
console.log('[rebuildAllDocs] Rebuilding config', i + 1, 'of', PROCESS_CONFIG.length, ':', config.triggerLabel);
var configCompleted = rebuildDoc(config);
if (!configCompleted) {
console.log('[rebuildAllDocs] Paused due to time constraints. Run rebuildAllDocs() again to continue.');
completed = false;
break;
}
}

if (completed) {
console.log('[rebuildAllDocs] Rebuild preparation complete.');
console.log('[rebuildAllDocs] Now run storeEmailsAndAttachments() to reprocess all emails.');
}
}

/**
* Rebuilds a single document by clearing it and moving processed emails back to trigger label.
* Uses batching and state tracking to handle large label sets without timing out.
* Returns true if completed, false if needs to continue in another execution.
*/
function rebuildDoc(config) {
var MAX_EXECUTION_TIME = 4 * 60 * 1000; // 4 minutes (leaving 2 min buffer for 6 min limit)
var BATCH_SIZE = 100; // Process 100 threads at a time
var startTime = new Date().getTime();

console.log('[rebuildDoc] Starting rebuild for:', config.triggerLabel);

var triggerLabelName = config.triggerLabel;
var processedLabelName = config.processedLabel;
var stateKey = 'rebuild_state_' + triggerLabelName.replace(/[^a-zA-Z0-9]/g, '_');

// 1. Validate and get labels
console.log('[rebuildDoc] Looking up labels');
var triggerLabel = GmailApp.getUserLabelByName(triggerLabelName);
var processedLabel = GmailApp.getUserLabelByName(processedLabelName);

if (!triggerLabel) {
console.error('[rebuildDoc] Trigger label not found:', triggerLabelName);
Logger.log("Trigger label not found: " + triggerLabelName);
return true; // Nothing to do, consider complete
}

if (!processedLabel) {
console.log('[rebuildDoc] Processed label not found:', processedLabelName, '- nothing to unarchive');
}

// 2. Check if we need to clear the document (only on first run)
var properties = PropertiesService.getUserProperties();
var rebuildState = properties.getProperty(stateKey);
var state = rebuildState ? JSON.parse(rebuildState) : { phase: 'clear_doc' };

if (state.phase === 'clear_doc') {
console.log('[rebuildDoc] Clearing document:', config.docId);
try {
var doc = DocumentApp.openById(config.docId);
var body = doc.getBody();

// Clear all content from the document body in a single operation
body.setText('');
console.log('[rebuildDoc] Document cleared');

// Move to next phase
state.phase = 'move_emails';
properties.setProperty(stateKey, JSON.stringify(state));
} catch (e) {
console.error('[rebuildDoc] Error clearing document:', e.message);
Logger.log("Error clearing document: " + e.message);
properties.deleteProperty(stateKey);
return true; // Error, consider done to avoid infinite loop
}
}

// 3. Move emails from processed label back to trigger label (batched)
if (state.phase === 'move_emails' && processedLabel) {
console.log('[rebuildDoc] Moving processed emails back to trigger label');

var allThreads = processedLabel.getThreads();
var totalThreads = allThreads.length;
console.log('[rebuildDoc] Found', totalThreads, 'processed threads remaining');

if (totalThreads === 0) {
// No threads to process, we're done
properties.deleteProperty(stateKey);
console.log('[rebuildDoc] No threads to move');
console.log('[rebuildDoc] Rebuild complete for:', config.triggerLabel);
console.log('[rebuildDoc] Run storeEmailsAndAttachments() to reprocess these emails');
return true;
}

// Process threads in batches, always starting from index 0
// (since we're removing threads as we go, the array shrinks)
var threadsToProcess = Math.min(BATCH_SIZE, totalThreads);
var threadsProcessed = 0;

for (var i = 0; i < threadsToProcess; i++) {
// Check if we're approaching time limit
var elapsed = new Date().getTime() - startTime;
if (elapsed > MAX_EXECUTION_TIME) {
console.log('[rebuildDoc] Approaching time limit, saving progress. Processed', threadsProcessed, 'threads this run');
properties.setProperty(stateKey, JSON.stringify(state));
return false; // Not completed, need another run
}

var thread = allThreads[i];
triggerLabel.addToThread(thread);
processedLabel.removeFromThread(thread);
threadsProcessed++;

if ((i + 1) % 10 === 0 || i === threadsToProcess - 1) {
console.log('[rebuildDoc] Moved', i + 1, 'of', threadsToProcess, 'threads in this batch');
}
}

console.log('[rebuildDoc] Processed', threadsProcessed, 'threads in this batch,', totalThreads - threadsToProcess, 'remaining');

if (threadsToProcess >= totalThreads) {
// All threads processed
properties.deleteProperty(stateKey);
console.log('[rebuildDoc] Moved all threads back to trigger label');
console.log('[rebuildDoc] Rebuild complete for:', config.triggerLabel);
console.log('[rebuildDoc] Run storeEmailsAndAttachments() to reprocess these emails');
return true;
} else {
// More threads to process
properties.setProperty(stateKey, JSON.stringify(state));
console.log('[rebuildDoc] Batch complete. Run rebuildAllDocs() again to continue.');
return false;
}
}

// If we got here with no processed label, we're done
properties.deleteProperty(stateKey);
console.log('[rebuildDoc] Rebuild complete for:', config.triggerLabel);
console.log('[rebuildDoc] Run storeEmailsAndAttachments() to reprocess these emails');
return true;
}

/**
* Processes a single configuration group (Label -> Doc + Folder).
*/
Expand Down Expand Up @@ -107,8 +265,11 @@ function processLabelGroup(config) {
try {
headingPara.setHeading(DocumentApp.ParagraphHeading.HEADING_3);
} catch (e) {
/* istanbul ignore next */
Comment thread
don-petry marked this conversation as resolved.
Outdated
var style = {};
/* istanbul ignore next */
style[DocumentApp.Attribute.BOLD] = true;
/* istanbul ignore next */
headingPara.setAttributes(style);
}

Expand Down Expand Up @@ -249,3 +410,8 @@ function getFileHash(blob) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}

// Export functions for testing (Node.js only)
if (typeof module !== 'undefined' && module.exports) {
module.exports = { rebuildDoc, rebuildAllDocs, processLabelGroup, storeEmailsAndAttachments };
}
Loading
Loading