Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save adamwolf/8852c618a1ca872b31b41d43ef5c0a4d to your computer and use it in GitHub Desktop.
Save adamwolf/8852c618a1ca872b31b41d43ef5c0a4d to your computer and use it in GitHub Desktop.
Gmail script filter based on search queries

Gmail Filter by Search Query

This program, in the form of a configuration script and a main script, allows for complicated Gmail search queries to be used as filters. It also lets you do more advanced stuff you can't do with ordinary filters, like label based on whether an email contains a specific kind of attachment.

Installing

  1. Go to script.google.com.
  2. Go to File > New > Script File, and type Main as the title for the new script. This will be for the main script.
  3. Delete any pre-filled text in the script file, and copy main.gs from this gist to that file.
  4. Go to File > New > Script File again, and type Config as the title for the new script. This will be for configuration.
  5. Delete any pre-filled text in the script file, and enter the desired configuration.
  6. Click Run > Run function > __install to install.
  7. If a dialog comes up to approve permissions, accept them. This script doesn't send anything to any server, nor does it attempt to do anything malicious behind your back.

Updating

If there's a bug, new feature, or something you want, you can update it pretty easily, too.

  1. Go to the script file you created when installing.
  2. Click the "Config" script and update it as necessary.
  3. Click the "Main" script.
  4. Copy the updated script into it.
  5. Click Run > Run function > __install to update.
  6. If a dialog comes up to approve permissions, accept them. This script doesn't send anything to any server, nor does it attempt to do anything malicious behind your back.

Uninstalling

If you, for any reason, wish to uninstall the script and revert back to what you used previously, here's how you do it.

  1. Go to the script file you created when installing.
  2. Click Run > Run function > __uninstall to uninstall.
  3. Delete your script from Google Apps Script if you wish.

Configuration

Configuration is a simple call to __setup({...}) with two options queries and notify. If you're non-technical, it's not as complicated as you might think. A basic configuration file might look like this:

__setup({
    // The queries to run filters over
    queries: [
        ["some query", function (thread) { /* ... */ }],
        ["another query", function (thread) { /* ... */ }],
    ],
    // Info for the weekly summary. Set to `false` instead to just do it in the
    // background.
    notify: {
        // The email the script runs under - must be a valid email.
        email: "[email protected]",
        // The subject for the weekly summary
        subject: "Filter Summary",
        // The email body as plain text - `%c` acts as a placeholder for the
        // filtered count
        body: "%c emails processed in the past 7 days.",
    },
});

queries is required to know which emails to filter, and the thread in each of those is a GmailThread for you to do things with. notify provides various options for the weekly summary, emailed Monday every week at 6 in the morning.

You can omit notify or just set it to true, in which it just defaults to this:

__setup({
    // ...
    notify: {
        email: "The email of the account you used to create the script",
        subject: "Weekly Filter Totals",
        body: "Number of emails successfully processed this past week: %c",
    },
});

You can also omit individual parts of notify (like email) to default to one of these. Note that if you specify notify without email and it can't detect the email for you, it will return an error and let you know it has to be specified explicitly.

Example configuration

Here's an example configuration based on my own one.

// Helper method. One caveat to be aware of is that you should not start
// variables with two underscores - those are reserved for internal use.
function trash(thread) {
  thread.moveToTrash();
}

__setup({
    queries: [
      ["in:all -in:trash category:social older_than:15d -is:starred", trash],
      ["in:all -in:trash category:updates older_than:15d -is:starred -label:Important-Label", trash],
      ["in:all -in:trash category:promotions older_than:15d -is:starred -label:Company-News", trash],
      ["in:all -in:trash category:forums older_than:90d -is:starred", trash],
    ],
});

Here's another configuration, one that just adds an already-created Company label to company emails with a more informative custom body:

__setup({
    queries: [
        ["from:[email protected]", function (thread) {
            thread.addLabel(GmailApp.getUserLabelByName("Company"));
        }],
    ],
    notify: {
        body: "%c threads from Company labeled",
    },
});

If you want to iterate emails, not just threads, use GmailThread#getMessages and iterate it like this:

function iterate(thread) {
    thread.getMessages().forEach(function (email) {
        // Do something with `email`
    });
}

This is useful for seeing if a thread has a PDF attachment and acting accordingly, something you can't do using Gmail's built-in filters:

__setup({
    queries: [
        ["in:all -in:trash -label:has-pdf", function (thread) {
            var hasPDF = false
            thread.getMessages().forEach(function (email) {
                email.getAttachments().forEach(function (attachment) {
                    if (attachment.getContentType() === "application/pdf") hasPDF = true;
                });
            });
            
            if (hasPDF) {
                thread.addLabel("Has PDF");
            }
        }],
    ],
});

Of course, if you're a programmer, you probably already noticed this could've been done a lot more efficiently:

// If you know JS well enough
__setup({
    queries: [
        ["in:all -in:trash -label:has-pdf", function (thread) {
            if (thread.getMessages().some(function (email) {
                return email.getAttachments().some(function (attachment) {
                    return attachment.getContentType() === "application/pdf";
                });
            })) {
                thread.addLabel("Has PDF");
            }
        }],
    ],
});

// Or maybe if you're used to Java or C++:
__setup({
    queries: [
        ["in:all -in:trash -label:has-pdf", function (thread) {
            var messages = thread.getMessages();
            for (var i = 0; i < messages.length; i++) {
                var attachments = messages[i].getAttachments();
                for (var j = 0; j < attachments.length; j++) {
                    if (attachments[j].getContentType() === "application/pdf") {
                        thread.addLabel("Has PDF");
                        return;
                    }
                }
            }
        }],
    ],
});

License

Blue Oak Model License 1.0.0. The full text is in main.gs as well as in LICENSE.md in this gist.

Blue Oak Model License

Version 1.0.0

Purpose

This license gives everyone as much permission to work with this software as possible, while protecting contributors from liability.

Acceptance

In order to receive this license, you must agree to its rules. The rules of this license are both obligations under that agreement and conditions to your license. You must not do anything with this software that triggers a rule that you cannot or will not follow.

Copyright

Each contributor licenses you to do everything with this software that would otherwise infringe that contributor's copyright in it.

Notices

You must ensure that everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license or a link to https://blueoakcouncil.org/license/1.0.0.

Excuse

If anyone notifies you in writing that you have not complied with Notices, you can keep your license by taking all practical steps to comply within 30 days after the notice. If you do not do so, your license ends immediately.

Patent

Each contributor licenses you to do everything with this software that would otherwise infringe any patent claims they can license or become able to license.

Reliability

No contributor can revoke this license.

No Liability

As far as the law allows, this software comes as is, without any warranty or condition, and no contributor will be liable to anyone for any damages related to this software or this license, under any kind of legal claim.

/**
* Copyright (c) 2019 and later, Isiah Meadows <[email protected]>.
* Source + Docs: https://gist.github.com/isiahmeadows/63716b78c58b116c8eb7
*
* # Blue Oak Model License
*
* Version 1.0.0
*
* ## Purpose
*
* This license gives everyone as much permission to work with this software as
* possible, while protecting contributors from liability.
*
* ## Acceptance
*
* In order to receive this license, you must agree to its rules. The rules of
* this license are both obligations under that agreement and conditions to your
* license. You must not do anything with this software that triggers a rule
* that you cannot or will not follow.
*
* ## Copyright
*
* Each contributor licenses you to do everything with this software that would
* otherwise infringe that contributor's copyright in it.
*
* ## Notices
*
* You must ensure that everyone who gets a copy of any part of this software
* from you, with or without changes, also gets the text of this license or a
* link to <https://blueoakcouncil.org/license/1.0.0>.
*
* ## Excuse
*
* If anyone notifies you in writing that you have not complied with Notices,
* you can keep your license by taking all practical steps to comply within 30
* days after the notice. If you do not do so, your license ends immediately.
*
* ## Patent
*
* Each contributor licenses you to do everything with this software that would
* otherwise infringe any patent claims they can license or become able to
* license.
*
* ## Reliability
*
* No contributor can revoke this license.
*
* ## No Liability
*
* ***As far as the law allows, this software comes as is, without any warranty
* or condition, and no contributor will be liable to anyone for any damages
* related to this software or this license, under any kind of legal claim.***
*/
/* global Session, LockService, Logger, PropertiesService, */
/* global ScriptApp, GmailApp */
/* exported __setup, __install, __uninstall, __runQueries, __emailResults */
var __i, __u, __r, __e;
function __install() { __i(); }
function __uninstall() { __u(); }
function __runQueries() { __r(); }
function __emailResults() { __e(); }
function __setup(opts) {
"use strict";
function writeLine(str) {
Logger.log('== Timed Filters == ' + (str != null ? str : ''));
}
writeLine('LOG: Initializing script.');
// Increment this any time a breaking change occurs
var ScriptVersion = 1;
// Helpful message in case of validation error
var invalidSuffix =
'Please fix this as soon as possible. Documentation for this script ' +
'can be found at ' +
'https://gist.github.com/isiahmeadows/63716b78c58b116c8eb7.';
function require(errs, name, obj, type) {
var optional = type.indexOf('?') >= 0;
if (obj == null && optional) return true;
type = type.replace(/[?\s]/g, '').split(/\|/g);
if (obj != null) {
for (var i = 0; i < type.length; i++) {
var test = type[i] === 'array' ? Array.isArray(obj)
: type[i] === 'integer' ? typeof obj === 'number' && obj % 1 === 0
: typeof obj === type[i];
if (test) return true;
}
}
if (type.length > 1) type[type.length - 1] = 'or ' + type[type.length - 1];
var msg = name + ' must be a' + (/^[aeiou]/.test(type[0]) ? 'n ' : ' ');
msg += type.join(type.length > 2 ? ', ' : ' ');
errs.push(msg + (optional ? ' when given. ' : '. ') + invalidSuffix);
return false;
}
var memoOptions;
function getOptions() {
if (memoOptions != null) return memoOptions;
var errs = [], notify;
require(errs, 'queries', opts.queries, 'array');
opts.queries.forEach(function (query) {
require(errs, 'query[0]', query[0], 'string');
require(errs, 'query[1]', query[1], 'function');
});
require(errs, 'notify', opts.notify, 'boolean? | object');
if (opts.notify != null && typeof opts.notify === 'object') {
notify = {
email: opts.notify.email,
subject: opts.notify.subject,
body: opts.notify.body,
};
if (
require(errs, 'notify.email', notify.email, 'string?') &&
notify.email == null
) {
notify.email = Session.getEffectiveUser().getEmail();
if (!notify.email) {
errs.push('Could not get user email - an explicit email option is ' +
'required. ' + invalidSuffix);
}
}
require(errs, 'notify.subject', notify.subject, 'string?');
require(errs, 'notify.header', notify.header, 'string?');
if (notify.subject == null) notify.subject = 'Weekly Filter Totals';
if (notify.body == null) {
notify.body = 'Number of threads successfully processed this past ' +
'week: %c';
}
}
if (errs.length) {
throw new TypeError(errs.join('\n'));
}
return memoOptions = {queries: opts.queries, notify: notify};
}
function task(name, isPriority, func) {
return function () {
var options = getOptions();
var properties = PropertiesService.getUserProperties();
var version = properties.getProperty('version');
if (version != null) {
version = +version;
} else if (properties.getProperty('total') != null) {
version = 0; // Let's phase in the old variant.
}
var state = {
options: options,
properties: properties,
version: version,
log: function (str) { writeLine('LOG: ' + str); },
error: function (str) { writeLine('ERROR: ' + str); },
};
writeLine(name);
// The lock is needed to make sure the callbacks aren't executed while we
// are setting them up, tearing them down, or if we're sending the summary
// email. 60 minutes should be well more than enough to run.
var lock = LockService.getUserLock();
var ms = 1000 /*ms*/ * 60 /*s*/ * (isPriority ? 10 : 60) /*min*/;
state.log('Waiting for lock...');
try {
lock.waitLock(ms);
} catch (_) {
if (isPriority) throw new Error('Failed to acquire lock.');
// A single lock failure isn't the end of the world here. The next
// scheduled run should be able to clean up after this.
state.error('Lock timed out or could not be accessed. Skipping this ' +
'run.');
state.error();
return;
}
var after;
try {
after = func(state);
} finally {
lock.releaseLock();
}
if (after) after();
state.log('Script executed successfully.');
writeLine();
};
}
__u = task('Uninstalling script.', true, function uninstall(state) {
state.log('Removing old triggers...');
state.log('Deleting properties...');
state.properties.deleteAllProperties();
// Old trigger type
ScriptApp.getProjectTriggers().forEach(function (trigger) {
var name = trigger.getHandlerFunction();
state.log('Removing trigger for function: ' + name);
ScriptApp.deleteTrigger(trigger);
});
});
__i = task('Installing script.', true, function install(state) {
if (state.version != null) {
if (state.version > ScriptVersion) {
throw new Error('To downgrade, fully uninstall and then reinstall. ' +
'Downgrading while retaining old data is not ' +
'supported.');
}
// Migrate if previously installed
state.log('Updating properties...');
state.properties.setProperty('version', ScriptVersion);
state.log('Updating triggers...');
// No triggers to migrate currently.
} else {
// Install from scratch
state.log('Installing properties...');
state.properties.setProperty('version', ScriptVersion);
state.properties.setProperty('total', '0');
state.log('Installing triggers...');
ScriptApp.newTrigger('__runQueries')
.timeBased()
.everyMinutes(10)
.create();
ScriptApp.newTrigger('__emailResults')
.timeBased()
.everyWeeks(1)
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(6)
.create();
}
});
__r = task('Running queries.', false, function runQueries(state) {
if (state.version == null) {
throw new Error('Please install (or reinstall) this script so this ' +
'task can run.');
}
var total = +state.properties.getProperty('total');
try {
state.options.queries.forEach(function (query) {
state.log('Executing query: ' + query[0]);
GmailApp.search(query[0]).forEach(function (thread) {
var subject = thread.getFirstMessageSubject();
state.log('Processing Gmail thread: ' + subject);
total++;
(0, query[1])(thread);
});
});
} finally {
state.properties.setProperty('total', '' + total);
}
});
__e = task('Emailing results.', true, function emailResults(state) {
if (state.version == null) {
throw new Error('Please install (or reinstall) this script so this ' +
'task can run.');
}
var notify = state.options.notify;
if (notify == null) return;
state.log('Generating email...');
var total = +state.properties.getProperty('total');
state.log('Previous total: ' + total);
state.log('Resetting total...');
state.properties.setProperty('total', '0');
return function () {
state.log('Sending weekly email...');
var body = notify.header.replace(/%c/g, total);
state.log('Email: ' + notify.email);
state.log('Subject: ' + notify.subject);
state.log('Body: ' + body);
GmailApp.sendEmail(notify.email, notify.subject, body);
state.log('Email sent successfully');
};
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment