Bulk-unsubscribe from senders you never open
Score senders by open rate over 90 days and stage unsubscribe actions for the worst offenders.
Published Dec 2, 2025
Marketing email accumulates quietly. A webinar sign-up here, a checkout there, and within a year the promotions tab is a wall of mail nobody reads. Clearing it by hand means opening each newsletter, scrolling to the tiny footer link and clicking through — tedious enough that most people just keep deleting instead.
Awadesh’s inbox at Northwind had reached exactly that state. This script does the boring part: it scans the last 90 days of promotional mail, scores every sender by how often their messages go unread, and writes the worst offenders — those he opens almost never — into a review Sheet, each with its unsubscribe link already extracted. He skims the Sheet once a quarter and clicks only the links he wants. The script never unsubscribes for him; it just removes the hunting.
What you’ll need
- A Google Sheet to hold the review list. The script writes to the first tab and clears it on each run, so use a dedicated, empty sheet.
- The Sheet’s ID, set as
REVIEW_SHEETin the config below. - Nothing else — the script reads your existing promotions mail and writes the results. It makes no changes to your inbox.
The script
// The Sheet that holds the review list. Use a dedicated, empty sheet — the
// first tab is cleared and rewritten on every run.
const REVIEW_SHEET = '1abcUnsubReviewSheetId';
// Only consider mail from the last 90 days.
const WINDOW = 'newer_than:90d';
// A sender must have sent at least this many messages to be ranked — one
// stray email is not a pattern.
const MIN_MESSAGES = 3;
// Flag a sender only if at least this fraction of their mail went unread.
const UNREAD_THRESHOLD = 0.9;
/**
* Scans promotional mail, scores each sender by unread rate, and writes the
* senders you almost never open into the review Sheet.
*/
function rankUnreadSenders() {
// email -> { total, unread, unsub }
const senders = new Map();
// 1. Pull every promotional thread from the last 90 days.
const threads = GmailApp.search('category:promotions ' + WINDOW);
if (!threads.length) {
Logger.log('No promotional mail in the window — nothing to do.');
return;
}
// 2. Tally each sender's totals from the first message of every thread.
for (const thread of threads) {
const msg = thread.getMessages()[0];
const from = msg.getFrom();
const stats = senders.get(from) || { total: 0, unread: 0, unsub: null };
stats.total++;
if (msg.isUnread()) stats.unread++;
// Grab the unsubscribe link once — the first message that has one wins.
if (!stats.unsub) stats.unsub = findUnsubLink(msg);
senders.set(from, stats);
}
// 3. Keep only senders that are frequent, almost never opened, and have a
// usable unsubscribe link. Sort the worst offenders to the top.
const rows = [...senders.entries()]
.filter(([, s]) =>
s.total >= MIN_MESSAGES
&& s.unread / s.total > UNREAD_THRESHOLD
&& s.unsub)
.sort((a, b) => b[1].total - a[1].total)
.map(([from, s]) => [from, s.total, s.unread, s.unsub]);
// 4. Rewrite the review Sheet from scratch with a header and one row each.
const sheet = SpreadsheetApp.openById(REVIEW_SHEET).getSheets()[0];
sheet.clear();
sheet.getRange(1, 1, 1, 4)
.setValues([['sender', 'sent', 'unread', 'unsubscribe']]);
if (rows.length) {
sheet.getRange(2, 1, rows.length, 4).setValues(rows);
}
Logger.log('Flagged ' + rows.length + ' sender(s) for review.');
}
/**
* Extracts the first unsubscribe / opt-out link from a message's HTML body.
* Returns the URL, or null if none is found.
*/
function findUnsubLink(msg) {
const body = msg.getBody();
const match = body.match(
/href="(https?:\/\/[^"]+(?:unsubscribe|opt[-_]?out)[^"]*)"/i,
);
return match ? match[1] : null;
}
How it works
rankUnreadSenderssearches Gmail forcategory:promotionsmail from the last 90 days. If there is none, it logs a message and stops.- For each thread it reads the first message and updates a per-sender tally: total messages, how many were unread, and an unsubscribe link captured the first time one appears.
- Once every thread is counted, it filters the senders down to those that meet all three tests — at least three messages, an unread rate above 90%, and a usable unsubscribe link — then sorts the highest-volume offenders first.
findUnsubLinkdoes the link extraction with a regular expression that looks for anhrefpointing at a URL containingunsubscribeoropt-out. This covers the overwhelming majority of legitimate marketing footers.- It clears the review Sheet, writes a header row, and drops one row per flagged sender: who they are, how much they sent, how much went unread, and the link to act on.
Example run
After a quarter of accumulated promotions, a run might write this to the review Sheet:
| sender | sent | unread | unsubscribe |
|---|---|---|---|
| Daily Deals [email protected] | 41 | 41 | https://dailydeals.example/unsub?u=8a2… |
| Webinar Hub [email protected] | 18 | 17 | https://webinarhub.example/opt-out/3f1… |
| Gadget Weekly [email protected] | 9 | 9 | https://gadgetweekly.example/unsubscribe… |
Three senders, 68 emails, almost none of them ever opened. Awadesh clicks the links he agrees with and ignores the rest — a two-minute job instead of an afternoon of footer-hunting.
Run it
This is a quarterly tidy-up, not a background job, so run it by hand:
- In the Apps Script editor, select
rankUnreadSendersand click Run. - Approve the authorisation prompt the first time.
- Open the review Sheet and click whichever unsubscribe links you want to action.
If you would rather not visit the editor, add a custom menu so it can be triggered from the Sheet itself:
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Inbox tools')
.addItem('Rank unread senders', 'rankUnreadSenders')
.addToUi();
}
Watch out for
- Gmail does not expose true open events to Apps Script. This script treats
isUnreadas a proxy for “ignored” — accurate enough for promotional mail you never click into, but noisy for anything you read in the preview pane without opening. - It only inspects the first message of each thread. A long promotional thread is rare, but where one exists only its opening message is scored.
- The unsubscribe regex is good, not perfect. Some senders bury the link behind
a redirect, an image, or a
List-Unsubscribeheader instead of a footer link — those senders simply will not appear in the Sheet. - A
category:promotionssearch over 90 days can return a large number of threads. If a run is slow or hits the script time limit, narrow the window tonewer_than:30dand run it more often. - The script never clicks anything. Unsubscribing is left entirely to you — which is the point, since some “promotions” are mail you actually want to keep.
Related
Convert long email threads into a summary note
Collapse a thread's history into a Doc for handover — perfect for client transitions or vacation cover.
Updated Jun 6, 2026
Pull event RSVPs from emails into a Sheet
Parse yes/no replies to event invites and tally attendance automatically.
Updated Jun 2, 2026
Turn forwarded emails into project tasks
Forward to [email protected] and a row lands in the Projects sheet under the right client.
Updated May 30, 2026
Turn starred emails into a task list
Sync every starred thread into the Northwind Tasks sheet automatically.
Updated May 26, 2026
Alert when a label hits a backlog threshold
Warn the Northwind team in Slack when a Gmail label has more than N unread threads.
Updated Mar 31, 2026