Sample Google Apps Script to send out bulk mails:
// ── CONFIG ────────────────────────────────────────────────────────────────
const SHEET_NAME = "Contacts";
const DAILY_LIMIT = 500; // ← Change to 2000 if you have Google Workspace
const FROM_NAME = "Ravi Rajamani";
// Column indices (1-based, matching your sheet layout)
const COL_EMAIL = 1; // Column A: to_email
const COL_SUBJECT = 7; // Column G: subject
const COL_BODY = 8; // Column H: body
const COL_STATUS = 9; // Column I: Status
// ── MAIN FUNCTION ─────────────────────────────────────────────────────────
function sendCampaign() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
Logger.log('ERROR: Sheet "' + SHEET_NAME + '" not found. Check the tab name.');
return;
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
Logger.log("No data rows found.");
return;
}
// ── READ ALL DATA IN ONE CALL (the key fix) ───────────────────────────
// This fetches the entire sheet as a 2D array in a single API call,
// instead of making 4 individual calls per row inside the loop.
const numRows = lastRow - 1; // exclude header row
const allData = sheet.getRange(2, 1, numRows, COL_STATUS).getValues();
// allData[i] is a 0-indexed array for row (i+2) of the sheet
// allData[i][0] = to_email, [6] = subject, [7] = body, [8] = Status
Logger.log("Data loaded. Total rows: " + numRows);
// ── COLLECT ROWS TO SEND ──────────────────────────────────────────────
// Build a list of {sheetRow, email, subject, body} for unsent rows only.
// This entire pass is pure in-memory — no Sheets API calls.
const toSend = [];
for (let i = 0; i < allData.length; i++) {
const status = String(allData[i][COL_STATUS - 1]).trim();
if (status === "Sent") continue; // already sent
if (status.startsWith("Error:")) continue; // skip permanent errors
const email = String(allData[i][COL_EMAIL - 1]).trim();
const subject = String(allData[i][COL_SUBJECT - 1]).trim();
const body = String(allData[i][COL_BODY - 1]).trim();
if (!email || !email.includes("@")) continue; // bad address
if (!subject || !body) continue; // incomplete row
toSend.push({ sheetRow: i + 2, email, subject, body }); // sheetRow is 1-based
}
Logger.log("Unsent rows found: " + toSend.length);
if (toSend.length === 0) {
Logger.log("Campaign complete — all rows have been sent.");
return;
}
// ── SEND EMAILS ───────────────────────────────────────────────────────
// Batch status updates: collect changes, then write them all at once
// at the end of the run (or every 50 sends as a safety checkpoint).
let sentThisRun = 0;
let errorCount = 0;
const statusUpdates = []; // {sheetRow, value}
const limit = Math.min(DAILY_LIMIT, toSend.length);
for (let j = 0; j < limit; j++) {
const { sheetRow, email, subject, body } = toSend[j];
try {
MailApp.sendEmail({
to: email,
subject: subject,
body: body,
name: FROM_NAME
});
statusUpdates.push({ sheetRow, value: "Sent" });
sentThisRun++;
if (sentThisRun % 50 === 0) {
// Checkpoint write every 50 sends so progress isn't lost if we time out
flushStatusUpdates(sheet, statusUpdates);
Logger.log("Checkpoint: " + sentThisRun + " sent so far...");
}
} catch (e) {
statusUpdates.push({ sheetRow, value: "Error: " + e.message });
errorCount++;
Logger.log("FAILED row " + sheetRow + " (" + email + "): " + e.message);
}
// 200ms pause to stay within Gmail's per-second rate limit
Utilities.sleep(200);
}
// ── FINAL WRITE ───────────────────────────────────────────────────────
// Write any remaining status updates not yet flushed
flushStatusUpdates(sheet, statusUpdates);
// ── SUMMARY ───────────────────────────────────────────────────────────
Logger.log("─────────────────────────────────────");
Logger.log("Run complete.");
Logger.log("Sent this run: " + sentThisRun);
Logger.log("Errors this run: " + errorCount);
Logger.log("Remaining: " + (toSend.length - sentThisRun));
Logger.log("─────────────────────────────────────");
}
// ── HELPER: batch-write all pending status updates ─────────────────────────
// Clears the array after writing so we don't double-write on checkpoints.
function flushStatusUpdates(sheet, updates) {
if (updates.length === 0) return;
for (const { sheetRow, value } of updates) {
sheet.getRange(sheetRow, COL_STATUS).setValue(value);
}
SpreadsheetApp.flush(); // force Sheets to commit writes immediately
updates.length = 0; // clear in-place so the caller's array is emptied
}
No comments:
Post a Comment