Skip to content

Instantly share code, notes, and snippets.

@szhu
Last active March 4, 2024 13:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save szhu/1d816086307c5de02bc9a2bb1cf01fe0 to your computer and use it in GitHub Desktop.
Save szhu/1d816086307c5de02bc9a2bb1cf01fe0 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Gmail Sender Utils
// @namespace https://github.com/szhu
// @match https://mail.google.com/mail/u/*
// @version 1.3
// @author Sean Zhu
// @description Quickly drill down by sender or label in Gmail.
// @homepageURL https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0
// @updateURL https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0/raw/gmail-sender-utils.user.js
// @downloadURL https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0/raw/gmail-sender-utils.user.js
// ==/UserScript==
// This script does two things:
//
// (1) Search messages from sender or label
// ----------------------------------------
//
// In a message list, if you click the email sender or label, you'll be taken to
// a listing of all emails from this sender or with this label.
// - By default, we search in the current scope. For example, if you're
// currently looking at a label and click on a sender, we'll search for all
// emails with this label and from this sender.
// - To find all emails from this sender, hold down Alt/Option when clicking.
//
// (2) Show top sender domains
// ---------------------------
//
// When you're at a messages list, we show all the top-level domains of the
// senders in the current view, sorted by number of emails. This is useful for
// seeing who is sending you a lot of email!
// - You can click on a domain to just see email from that domain.
//
// How to install
// ==============
//
// RECOMMENDED INSTALLATION:
// 1. Install Tampermonkey: https://www.tampermonkey.net/
// 2. On this page, click Raw.
// 3. Tampermonkey should prompt you to install this userscript.
//
// IN CASE THAT DOESN'T WORK:
// 1. Install an extension that lets you run userscripts.
// 2. Create a new userscript, and paste this entire file.
//
// ALTERNATIVE INSTALLATION: If you don't want to install a extension just to be
// able to use this, you can install this tool as an extension itself:
// 1. Click "Download as ZIP".
// 2. Go to chrome://extensions, and turn on Developer Mode.
// 3. Drag the entire unzipped folder into the Chrome window.
//
// Installing as an extension works in Chromium browsers (Chrome, Edge, Opera,
// etc.). It may work in Firefox (please leave a comment if it doesn't). It will
// not work in Safari.
//
// After you're done, this script will work the next time you open a Gmail tab!
/**
* Create a css`` template literal that doesn't do anything. This is useful for
* syntax highlighting in editors.
*/
function css(strings, ...expressions) {
let result = strings[0];
for (let i = 1, l = strings.length; i < l; i++) {
result += expressions[i - 1];
result += strings[i];
}
return result;
}
document.addEventListener(
"click",
(e) => {
let initialQuery = document.querySelector('[name="q"').value.split(" ")[0];
if (!initialQuery && location.hash === "#inbox") {
initialQuery = "in:inbox";
}
let additionalQuery;
let labelEl = e.target.closest(".ar.as > .at");
if (labelEl) {
let label = labelEl.getAttribute("title");
console.log(label);
additionalQuery = `label:${label.replace(/[ {}&"()/|]/g, "-")}`;
console.log(additionalQuery);
}
let emailEl = e.target.closest("[email]");
if (emailEl) {
additionalQuery = [
"from:(" + emailEl.getAttribute("email") + ")",
"to:(" + emailEl.getAttribute("email") + ")",
].join(" OR ");
}
if (additionalQuery) {
let query = [e.altKey ? "" : initialQuery, additionalQuery].join(" ");
location.hash =
`#search/` + encodeURIComponent(query).replace(/%20/g, "+");
e.stopPropagation();
}
},
true
);
/**
* Create element.
*
* @param {{
* tag?: string;
* children?: (string | Element)[];
* [key: string]: any;
* } | string} tagChildrenAttributes
* @param {(string | Element)[]} moreChildren
*/
function El(tagChildrenAttributes, ...moreChildren) {
let {
tag = "div",
children = [],
...attributes
} = typeof tagChildrenAttributes === "string"
? { tag: tagChildrenAttributes }
: tagChildrenAttributes;
// Create element.
let el = document.createElement(tag);
// Set attributes.
for (let [key, value] of Object.entries(attributes)) {
if (key.startsWith("on") && typeof value === "function") {
el.addEventListener(key.slice(2), value);
} else if (value == null || typeof value === "boolean") {
if (value) {
el.setAttribute(key, "");
}
} else {
el.setAttribute(key, value);
}
}
// Add children.
for (let child of [...children, ...moreChildren]) {
el.appendChild(
typeof child === "string" //
? document.createTextNode(child)
: child
);
}
return el;
}
function Counter(array) {
let count = {};
counterAdd(count, array);
}
function counterAdd(count, array) {
array.forEach((val) => (count[val] = (count[val] || 0) + 1));
return count;
}
let lastScan = "";
let scan = () => {
if (document.querySelector("#email-frequency-lock")?.checked) {
return;
}
console.log("Scan triggered");
let emails = [];
for (let el of document.querySelectorAll(
'[role="main"] [jsmodel="nXDxbd"] .yW [email]'
)) {
let email = el.getAttribute("email");
let domain = email.match("(.*@)?(.*)")[2];
// Remove all but the last two parts of the domain
domain = domain.split(".").slice(-2).join(".");
emails.push(email);
}
if (JSON.stringify(emails) === lastScan) {
console.log("Same results as last time.");
return;
}
lastScan = JSON.stringify(emails);
let resetCounts = !document.querySelector("#email-frequency-keep")?.checked;
console.log(resetCounts);
if (!window.emailAddressCounts || resetCounts) {
window.emailAddressCounts = {};
}
window.emailAddressCounts = counterAdd(window.emailAddressCounts, emails);
let entries = Object.entries(window.emailAddressCounts).sort((a, b) => {
let dCount = b[1] - a[1];
if (dCount !== 0) {
return dCount;
}
function domainFirst(email) {
return email.replace(/^(.*)@([^@]+)$/, "$2: $1");
}
return domainFirst(a[0]).localeCompare(domainFirst(b[0]));
});
console.log(
entries
.slice(0, 10)
.map(([email, count]) => {
return `${count}: ${email}`;
})
.join("\n")
);
if (entries.length <= 1) {
return;
}
let tbody = document.querySelector("#email-frequency-container tbody");
if (!tbody) {
let container = El(
{
tag: "div",
id: "email-frequency-container",
class: "TO",
style: css`
max-height: 30%;
overflow-y: auto;
padding: 10px 22px;
margin-bottom: 13px;
font: 0.875rem Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
`,
},
El(
{
tag: "table",
style: css`table-layout: fixed; width: 100%;`,
},
El(
{ tag: "thead", class: "nU", style: "display: table-row-group;" },
// The table's column widths come from this row.
El(
{ tag: "tr", class: "n0" },
El({
tag: "td",
style: css`width: 2ch; padding-right: 2ch;`,
}),
El("td")
),
El(
{
tag: "tr",
style: css`
cursor: default;
`,
class: "aAv",
},
El(
{ tag: "td", colspan: 2 },
El(
{
tag: "div",
style: "display: flex; align-items: center; gap: 0.5ch;",
},
"Senders",
El({ tag: "div", style: "flex-grow: 1" }),
El(
{
tag: "label",
style:
"font-size: smaller;; cursor: pointer; display: inline-flex; gap: 0.5ch;",
},
El({
tag: "input",
type: "checkbox",
id: "email-frequency-keep",
}),
"Keep"
),
El(
{
tag: "label",
style:
"font-size: smaller;; cursor: pointer; display: inline-flex; gap: 0.5ch;",
},
El({
tag: "input",
type: "checkbox",
id: "email-frequency-lock",
}),
"Lock"
)
)
)
)
),
El({ tag: "tbody", class: "nU", style: "display: table-row-group;" })
)
);
document
.querySelector(".V3.aam")
.insertAdjacentElement("afterend", container);
tbody = container.querySelector("tbody");
}
while (tbody.firstChild) {
tbody.firstChild.remove();
}
tbody.append(
...entries.map(([email, count]) =>
El(
{
tag: "tr",
email,
style: css`
cursor: pointer;
`,
class: "n0",
},
El({
tag: "td",
style: css`text-align: right; width: 2ch; padding-right: 2ch;`,
children: ["" + count],
}),
El({
tag: "td",
style: css`overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap;`,
children: [email],
})
)
)
);
return entries;
};
function watchElementForChanges(el, callback) {
let observer = new MutationObserver(callback);
observer.observe(el, {
childList: true,
subtree: true,
});
}
async function main() {
let container;
while (!container) {
container = document.querySelector(`[role="navigation"] + *`);
await new Promise((resolve) => setTimeout(resolve, 100));
}
watchElementForChanges(container, scan);
scan();
console.log("Watching container:", container);
}
main();
console.log("Userscript loaded.");
{
"manifest_version": 3,
"name": "Gmail Sender Utils",
"description": "Features to help you identify and sort through senders in Gmail.",
"version": "1.3",
"permissions": [],
"host_permissions": [
"https://mail.google.com/mail/u/*"
],
"content_scripts": [{
"js": ["gmail-sender-utils.user.js"],
"matches": ["https://mail.google.com/mail/u/*"]
}]
}
@f-steff
Copy link

f-steff commented Jan 1, 2024

Thank you for this excellent userscript. It's working really well!

May I suggest that you add something like this to the header, to automate updates when you make changes?

// @homepageURL  https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0
// @downloadURL  https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0/raw/filter-gmail-unread.user.js
// @updateURL  https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0/raw/filter-gmail-unread.user.js

Thanks in advance.

@szhu
Copy link
Author

szhu commented Jan 3, 2024

I didn't know about this, thank you for the suggestion!

btw, I was curious about the difference between @downloadURL and @updateURL, and I found this:
https://stackoverflow.com/a/38025376/782045

Here's what I learned from that answer:

  • @updateURL is useful for scripts that are so large that always fetching the entire script would be expensive. @updateURL can point to a script that only has the metadata.
  • If @updateURL isn't defined, @downloadURL is used for both checking for an update and downloading the update.
  • For Tampermonkey, I have to increment the version number for users to receive the update.

Based on that, I added this to the metadata:

// @homepageURL  https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0
// @downloadURL  https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0/raw/gmail-sender-utils.user.js

I didn't have time to verify any of this. If it's not working please let me know.

@f-steff
Copy link

f-steff commented Jan 4, 2024

Thank you so much.
I, tested it in TamperMonkey, but unfortunately could not perform the update verification as automatic updates are disabled when the script is edited locally.

I also didn't know the particular meaning of @downloadURL and @updateURL, but I notice that the TamperMonkey editor complains if one and not the other is defined. I also noticed that it complains about a missing @description, too.

Would you consider yet another modification, adding the below?

// @description  With a click, list all gmail messages send to or from an email address.
// @updateURL  https://gist.github.com/szhu/1d816086307c5de02bc9a2bb1cf01fe0/raw/gmail-sender-utils.user.js

Thanks in advance.

@szhu
Copy link
Author

szhu commented Jan 14, 2024

Just updated the script, thanks. Let me know if it's working.

@f-steff
Copy link

f-steff commented Jan 20, 2024

Thanks a lot!
I no longer have the complaints in the TamperMonkey script editor.
And it seems I can now perform a manual check for script updates.
Looks perfect!
I'm looking forward to test the automatic update detection next time you update the script!

@szhu
Copy link
Author

szhu commented Mar 4, 2024

Hey @f-steff, today I noticed that Gmail made a change that prevented this script from working properly, and I updated this gist with a fix.

I can finally confirm that if I go to TamperMonkey » Gmail Sender Utils and choose File » Check for updates, the userscript successfully updates to the latest version. Thanks for helping me set it up!

@f-steff
Copy link

f-steff commented Mar 4, 2024

Good work @szhu . Updated flawlessly and have no warnings. Works perfectly, too. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment