/**
const CONFIG = { LABEL: "unsubscribe", FAIL_LABEL: "unsubscribe-fail",
MOVE_TO_SPAM_ON_FAIL: false, MARK_READ_ON_SUCCESS: true, REMOVE_LABEL_ON_SUCCESS: true,
SEARCH_QUERY: "label:unsubscribe",
RETRY_ATTEMPTS: 3, RETRY_DELAY_MS: 3000, // 3 seconds THROTTLE_MS: 1200, // 1.2 seconds between fetches
SUCCESS_PHRASES: [ // English "you have been unsubscribed", "unsubscription confirmed", "successfully unsubscribed", "your email has been removed", "you've been removed from our list", "opt-out successful", "you will no longer receive emails from us", "we're sorry to see you go", "your subscription has been canceled", "your request has been processed", "unsubscribed from our mailing list", "you've unsubscribed", "you’ve unsubscribed",
// Swedish
"du är avregistrerad",
"avregistreringen bekräftad",
"du har avregistrerat dig",
"du har avanmält dig",
"du är avanmäld",
"avregistrering lyckades",
"avanmälan lyckades"] };
let lastFetchTime = 0; // for throttling
/**
const failLabel = GmailApp.getUserLabelByName(CONFIG.FAIL_LABEL) || GmailApp.createLabel(CONFIG.FAIL_LABEL);
const threads = GmailApp.search(CONFIG.SEARCH_QUERY);
for (let thread of threads) { const messages = thread.getMessages(); let handled = false;
for (let msg of messages) {
const body = msg.getBody();
const unsubscribeLink = getEmailUnsubscribeLink(body);
if (unsubscribeLink) {
Logger.log("Found unsubscribe link: " + unsubscribeLink);
const result = followUnsubscribeLinkWithRetry(unsubscribeLink);
finalizeThread(thread, msg, result);
handled = true;
break;
}
// Attempt List-Unsubscribe header
const raw = msg.getRawContent();
if (!raw) {
Logger.log("Raw content missing -> FAIL");
finalizeThread(thread, msg, false);
handled = true;
break;
}
const listURL = RawListUnsubscribe(raw);
if (listURL) {
Logger.log("List-Unsubscribe: " + listURL);
const result = fetchWithRetry(listURL);
finalizeThread(thread, msg, result);
handled = true;
break;
}
// No unsubscribe found
Logger.log("No unsubscribe options detected: " + msg.getFrom());
finalizeThread(thread, msg, false);
handled = true;
break;
}
if (!handled) Logger.log("Thread had no messages to process.");} }
/**
if (success) { Logger.log("SUCCESS: " + msg.getFrom()); if (CONFIG.REMOVE_LABEL_ON_SUCCESS) thread.removeLabel(mainLabel); if (CONFIG.MARK_READ_ON_SUCCESS) msg.markRead(); return; }
// Failure Logger.log("FAIL: " + msg.getFrom());
thread.addLabel(failLabel); if (CONFIG.MOVE_TO_SPAM_ON_FAIL) thread.moveToSpam(); if (CONFIG.REMOVE_LABEL_ON_SUCCESS) thread.removeLabel(mainLabel);
msg.markRead(); }
/**
const urls = header[1].match(/https?:\/\/[^>,"\s]+/g); return urls ? urls[0] : null; }
/**
const segment = body.substring(idx); const link = segment.match(/<a\s+[^>]href=["'](.?)["']/i); return link ? link[1] : null; }
/**
for (let attempt = 1; attempt <= CONFIG.RETRY_ATTEMPTS; attempt++) {
Logger.log(Attempt ${attempt}: ${link});
const content = safeFetch(link);
if (content !== null) {
return checkUnsubscribeSuccess(content);
}
if (attempt < CONFIG.RETRY_ATTEMPTS) Utilities.sleep(CONFIG.RETRY_DELAY_MS);}
return false; }
/**
======================
GENERIC RETRY FETCH
====================== */ function fetchWithRetry(url) { for (let attempt = 1; attempt <= CONFIG.RETRY_ATTEMPTS; attempt++) {
Logger.log(Attempt ${attempt}: ${url});
const content = safeFetch(url);
if (content !== null) return true;
if (attempt < CONFIG.RETRY_ATTEMPTS) Utilities.sleep(CONFIG.RETRY_DELAY_MS); } return false; }
/**
======================
THROTTLED SAFE FETCH
====================== */ function safeFetch(url) { try { throttle();
const response = UrlFetchApp.fetch(url, { followRedirects: true, muteHttpExceptions: true });
const code = response.getResponseCode(); if (code >= 200 && code < 400) { return response.getContentText(); }
Logger.log("Fetch error code: " + code); return null;
} catch (err) { Logger.log("Fetch exception: " + err); return null; } }
/**
if (diff < CONFIG.THROTTLE_MS) { Utilities.sleep(CONFIG.THROTTLE_MS - diff); }
lastFetchTime = Date.now(); }
/**
// 1. Generic English detection if (lower.includes("unsubscribed")) { if (!lower.includes("not unsubscribed") && !lower.includes("failed")) { return true; } }
// 2. Generic Swedish detection // Matches: "avanmäl", "avanmäld", "avregistr", etc. if (lower.includes("avanmäl") || lower.includes("avregistr")) { // Block false-negatives: "kunde inte avregistrera" etc. if (!lower.includes("misslyck") && // failed !lower.includes("kunde inte") && // could not !lower.includes("ej")) { // not return true; } }
// 3. Phrase list fallback (English + Swedish) return CONFIG.SUCCESS_PHRASES.some(p => lower.includes(p)); }
Raw Paste