Untitled

Markdown Detected Guest 8 Views Size: 6.94 KB Posted on: Dec 2, 25 @ 9:37 AM

/**

  • ======================
  • CONFIG
  • ====================== */

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

/**

  • ======================
  • MAIN FUNCTION
  • ====================== */ function unsubscribeFromEmails() { const mainLabel = GmailApp.getUserLabelByName(CONFIG.LABEL) || GmailApp.createLabel(CONFIG.LABEL);

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.");

} }

/**

  • ======================
  • FINALIZE THREAD
  • ====================== */ function finalizeThread(thread, msg, success) { const mainLabel = GmailApp.getUserLabelByName(CONFIG.LABEL); const failLabel = GmailApp.getUserLabelByName(CONFIG.FAIL_LABEL);

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(); }

/**

  • ======================
  • LIST-UNSUBSCRIBE PARSER
  • ====================== / function RawListUnsubscribe(rawContent) { const header = rawContent.match(/^List-Unsubscribe:\s(.+)$/im); if (!header) return null;

const urls = header[1].match(/https?:\/\/[^>,"\s]+/g); return urls ? urls[0] : null; }

/**

  • ======================
  • HTML LINK FINDER
  • ====================== */ function getEmailUnsubscribeLink(body) { const idx = body.toLowerCase().indexOf("unsubscribe"); if (idx === -1) return null;

const segment = body.substring(idx); const link = segment.match(/<a\s+[^>]href=["'](.?)["']/i); return link ? link[1] : null; }

/**

  • ======================
  • RETRY + THROTTLE LAYER
  • ====================== */ function followUnsubscribeLinkWithRetry(link) { if (!link.startsWith("http")) return false;

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; } }

/**

  • ======================
  • THROTTLE FUNCTION
  • ====================== */ function throttle() { const now = Date.now(); const diff = now - lastFetchTime;

if (diff < CONFIG.THROTTLE_MS) { Utilities.sleep(CONFIG.THROTTLE_MS - diff); }

lastFetchTime = Date.now(); }

/**

  • ======================
  • GENERIC SUCCESS DETECTION
  • ====================== */ function checkUnsubscribeSuccess(content) { const lower = content.toLowerCase();

// 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

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA), and — with your consent — to measure usage and show ads. See Privacy.