Skip to content

Instantly share code, notes, and snippets.

@szhu
Last active September 17, 2024 01:58
Show Gist options
  • 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/*"]
}]
}
@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. :-)

@archon810
Copy link

Thank you, this is working perfectly. I posted about it here https://twitter.com/ArtemR/status/1793451028442206291.

@szhu
Copy link
Author

szhu commented May 23, 2024

Thanks, glad to hear it!

btw, if anyone finds the Tampermonkey install flow too cumbersome, please let me know. I'm considering publishing this to the Chrome Web Store as well, but only if people would find it helpful.

@archon810
Copy link

You should do it. Many more people will find it this way.

@f-steff
Copy link

f-steff commented Jul 10, 2024

btw, if anyone finds the Tampermonkey install flow too cumbersome, please let me know. I'm considering publishing this to the Chrome Web Store as well, but only if people would find it helpful.

I find Tampermonkey very easy to use. But I'm sure it will be easier for most ordinary people (like my mom) to find and use if it's available on the Chrome Web Store.

Did you find a conversion guide somewhere (I would be interested, too), or do you have to rewrite it from scratch?

@szhu
Copy link
Author

szhu commented Jul 10, 2024

From a technical perspective, it's very easy to convert userscripts to Chrome extensions: all you have to do is add a manifest.json file. In fact, this conversion has already been done. The manifest and the install instructions have been present since day one.

My hesitations have to do with publishing the extension to the Chrome Web Store:

  • Generating a suitable screenshot. The Chrome Web Store requires developers to upload a screenshot of a specific set of dimensions to be able to submit the extension.
  • Handling support. I expect that changes to Gmail will break the extension from time to time. If this gist is broken, people will just comment on this gist. But if this extension is on the Web Store, I have no interest in logging into the web store console periodically just to see if anyone left any 1-star ratings indicating that the extension doesn't work. I'd rather have people open a GitHub issue, email me, or submit a feedback form (which I would set up to email me). So I need to figure out how to correctly set up the feedback loop once this extension is published. (And not just for my own sake as a developer; I also personally dislike installing an extension just to realize that it doesn't work.)

I'm already thinking through these, but also welcoming any input here.

@f-steff
Copy link

f-steff commented Jul 10, 2024

I need to look further into this as well. Thanks for sharing.

I understand the screenshot could be difficult, but also doable. If you are concerned with too much text, then just show a slice of the Gmail GUI, and your added functionality.
You have 5 images available. I'd consider using one of them to explain to contact you if it's not working - I notice the Chrome Web Store has both a link to a website, and an email. So the website could be linked to the project GitHub page and the email to your email.

I'm looking forward to following your progress on this.

@efighter
Copy link

First off this is a great extension. I cannot even think about using GMail witout it. Thanks. I however need a little help utilizing the functionality listed in #2

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

I am in a message list but i feel i am missing where to click on a domain but also i do not see anything that sorts by number of emails or lists number of emails. thanks.

GmScreenshot

@szhu
Copy link
Author

szhu commented Sep 12, 2024

Thank you for the kind words!

I am in a message list but i feel i am missing where to click on a domain but also i do not see anything that sorts by number of emails or lists number of emails. thanks.

The top senders feature is in the bottom-left corner.

It's only visible when the left sidebar is expanded (i.e., when you can see the names of your labels).

image

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

I just noticed that my documentation for this feature is a bit outdated.

A few months ago I changed the feature from showing top sender top-level domains to showing top sender email addresses.

I forget the exact reasoning for this, but I think it might have been because for some domains I want to view all the email addresses separately. (For example, [email protected] and [email protected] are very different senders.) The ideal design would be a two-level filter where you can expand each domain to view its email addresses, but I haven't gotten around to that yet.

I hope the top-sender-addresses feature is useful as is, but let me know if the top-sender-domains feature would be significantly more useful.

@efighter
Copy link

@szhu

Thanks for replying with the information.

  1. I totally agree with you on showing unique email. Ive wanted that in Gmail since the beginning. Its kinda like in Outlook when you sort by sender and collapse the group it shows you where you have the most emails.

  2. I think i know what is going on. It appears i am generating an error when the script is attempting to add the results to the end of the container.(Ln #304)

f12console001

I believe it is due to the fact that i have 188 labels and most of them are displayed in the left column. but i dont know about the root cause for sure it is just an assumption.

I am happy to work with you on this to try to figure it out if you like. but its not critical. Ill just open the console and read it there to help me clean out some emails.

@szhu
Copy link
Author

szhu commented Sep 12, 2024

Wow thanks for all the helping debugging info. Note that I have a ridiculous number of labels too, so it might not necessary be that.

I don't have time soon to figure this out but will it revisit when I can, glad to hear that it's not urgent and that there's a workaround.

@efighter
Copy link

no worries at all. if it works on your end then thats all that matters! LOL

the major functionality is really what helps me the most so i am good to go!

Just post here if you want any assistance.

@f-steff
Copy link

f-steff commented Sep 13, 2024

Just as @efighter I've never used that function, but now that I understand what I'm missing out on, I wanted to test it out. :-)

It turns out I can't see it either, and looking in the debug output, I have exactly the same error.
I have about 1200 labels and sub-labels.

I'll be happy to assist debugging/testing, too.

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