Skip to content

Instantly share code, notes, and snippets.

@Excedrin
Last active July 16, 2026 00:01
Show Gist options
  • Select an option

  • Save Excedrin/5ac9bf902e277187a405c3b9bc525306 to your computer and use it in GitHub Desktop.

Select an option

Save Excedrin/5ac9bf902e277187a405c3b9bc525306 to your computer and use it in GitHub Desktop.
Filter gmail messages using app script based on mail headers
// To deploy: paste into a new project at script.google.com, adjust the RULES, run
// installTrigger() once, and approve the Gmail OAuth prompt.
// ==== Debug ====
// Logs go to the Executions view (left sidebar in the Apps Script editor).
var DEBUG_LOG = false; // note: JS lowercase false, not False
// ==== Rules ====
// header -> value regex -> list of actions.
// Regexes are unanchored (use ^...$ to match the whole value) and
// case-insensitive. "." means "header is present with any value".
// Each action is a function(thread, message). Add your own freely.
var RULES = {
"X-Custom-Header": {
"some-custom-value": [label("Some Label")],
".": [label("Has custom header")], // header present, any value
},
"List-Id": {
"foo-users\\.lists\\.example\\.com": [label("Foo ML"), archive],
},
"Auto-Submitted": {
"^auto-generated$": [markRead],
},
};
// ==== Action helpers ====
// label() is a factory returning an action; the rest are plain actions.
function label(name) {
return function (thread, message) {
var l = GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
thread.addLabel(l); // labels apply to threads, not messages
};
}
function archive(thread, message) { thread.moveToArchive(); }
function markRead(thread, message) { message.markRead(); }
function markImportant(thread, message) { thread.markImportant(); }
function star(thread, message) { message.star(); }
function trash(thread, message) { thread.moveToTrash(); }
function forwardTo(address) {
return function (thread, message) { message.forward(address); };
}
// ==== Main entry point (run by trigger every 10 min) ====
function processInbox() {
var props = PropertiesService.getScriptProperties();
var lastRun = Number(props.getProperty("lastRun")) || (Date.now() - 3600 * 1000);
var now = Date.now();
// Gmail's after: only has second granularity; overlap by 5 min to be safe.
// Actions should be idempotent (label/archive/markRead all are).
var afterEpoch = Math.floor(lastRun / 1000) - 300;
var query = "in:inbox after:" + afterEpoch;
var threads = GmailApp.search(query);
var stats = { examined: 0, skippedOld: 0, matched: 0, ruleHits: {} };
for (var i = 0; i < threads.length; i++) {
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++) {
// Skip messages older than the window (threads match if ANY message is new)
if (messages[j].getDate().getTime() < afterEpoch * 1000) {
stats.skippedOld++;
continue;
}
stats.examined++;
applyRules(threads[i], messages[j], stats);
}
}
console.log(
"%s messages examined (%s threads, %s old messages skipped), %s matched a rule. Window: %s -> %s (query: %s)",
stats.examined, threads.length, stats.skippedOld, stats.matched,
new Date(afterEpoch * 1000).toISOString(), new Date(now).toISOString(), query
);
if (DEBUG_LOG) {
for (var rule in stats.ruleHits) {
console.log("rule [%s]: %s match(es)", rule, stats.ruleHits[rule]);
}
}
props.setProperty("lastRun", String(now));
}
function applyRules(thread, message, stats) {
for (var headerName in RULES) {
var headerValue = message.getHeader(headerName);
if (!headerValue) {
if (DEBUG_LOG) {
console.log('msg "%s": header %s absent/empty', message.getSubject(), headerName);
}
continue;
}
var valueMap = RULES[headerName];
for (var pattern in valueMap) {
var ruleKey = headerName + " ~ /" + pattern + "/";
if (new RegExp(pattern, "i").test(headerValue)) {
stats.matched++;
stats.ruleHits[ruleKey] = (stats.ruleHits[ruleKey] || 0) + 1;
if (DEBUG_LOG) {
console.log('msg "%s": %s MATCHED value "%s", running %s action(s)',
message.getSubject(), ruleKey, headerValue, valueMap[pattern].length);
}
var actions = valueMap[pattern];
for (var k = 0; k < actions.length; k++) {
actions[k](thread, message);
}
} else if (DEBUG_LOG) {
console.log('msg "%s": %s no match (value: "%s")',
message.getSubject(), ruleKey, headerValue);
}
}
}
}
// ==== Run once manually to install the trigger ====
function installTrigger() {
// Remove existing triggers for this function to avoid duplicates
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === "processInbox") ScriptApp.deleteTrigger(t);
});
ScriptApp.newTrigger("processInbox").timeBased().everyMinutes(10).create();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment