Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save TheMetaphysicalCrook/a5f781704e97d33c4a9efe4810b0abb1 to your computer and use it in GitHub Desktop.
Save TheMetaphysicalCrook/a5f781704e97d33c4a9efe4810b0abb1 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 Gmail search queries to be used for the equivalent of filters.

How to use:

  1. Go to script.google.com.
  2. Go to File > New > Script File, and type Main or any other name of your choosing 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 or any other name of your choosing for the title. This will be for the configuration file.
  5. Delete any pre-filled text in the script file, and enter the desired configuration options for the file. There are two sample configuration files at the end of this readme.

Configuration options

Note that the configuration files are technically JavaScript files in non-strict mode. The easiest way to assign the variables are through implicit globals. The main requirement is that they are installed into the global namespace, so the utility can read them.

queries

  • Type: Array<[query: string, callback: function(thread: GmailThread)]>
  • Required

The search queries to run each iteration. These are the same search queries you type in the search bar. Note that it's an array of pairs, left a string and right a callback accepting a single GmailThread as an argument. This callback is called for each matching thread for the query.

queryInterval

  • Type: number
  • Default Value: 10

The number of minutes between each iteration.

notifyEmail

  • Type: boolean
  • Default Value: true

Whether to send the weekly email. Set to false to disable it.

subject

  • Type: string
  • Default Value: "Weekly Deletion Totals"

The weekly email subject.

header

  • Type: string
  • Default Value: "Number of emails successfully deleted this past week: {}"

The weekly email body, where "{}" is a placeholder for the total.

emailWeekday

  • Type: string
  • Default Value: Monday

The day of the week to send the email. This is not case-sensitive. Note that this is limited to one of the following values, with no whitespace involved:

  • Monday
  • Tuesday
  • Wednesday
  • Thursday
  • Friday
  • Saturday
  • Sunday

emailHour

  • Type: string|number
  • Default Value: 1, 1am, 1a, etc.

The hour of the day to send the email. It is in one of the following formats:

  • an integer from 0 to 23, representing an hour in a 24 hour clock.
  • "1 am" or "1am", representing an hour on a 12-hour clock (likewise, "1 pm" or "1pm")
  • "1 a" or "1a", representing an hour on a 12-hour clock (likewise, "1 p" or "1p")

In the first case, it expects a number or a string. In the other two, it expects only a string.

email

  • Type: string
  • Default Value: your email address, script fails if it cannot be found.

Example configuration files

Here's a basic sample, based on my own one.

queries = [
  // Format: ['query', actionCallback(thread: GmailThread)]
  ['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]
];

queryInterval = 10;

// Helper method. One caveat to be aware of is that defined functions and
// declared variables should not clash with option names.

function trash(thread) {
  thread.moveToTrash();
}

Here's one with queries and all the default options:

queries = [
  // Format: ['query', actionCallback(thread: GmailThread)]
  ['in:all -in:trash category:social older_than:15d -is:starred', trash]
];

queryInterval = 10;
notifyEmail = true;
subject = "Weekly Deletion Totals";
header = "Number of emails successfully deleted this past week: {}";
emailWeekday = Monday;
emailHour = 1;

function trash(thread) {
  thread.moveToTrash();
}

Identifiers that will conflict at the global level

This goes beyond configuration option names.

  • Anything prefixed with two underscores is reserved for implementation details.
  • initialize
  • runQueries
  • emailResults

License

BSD 2-Clause (full text included in main.gs within this gist)

/**
* Copyright (c) 2015, Isiah Meadows
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Original source: https://gist.github.com/isiahmeadows/63716b78c58b116c8eb7
*/
/* jshint maxlen: 80, indent: 2, eqnull: true, freeze: true, eqeqeq: true,
es3: true, unused: true */
/* global Session, LockService, Logger, PropertiesService, ScriptApp,
GmailApp, __i, __r, __e */
/* exported initialize, runQueries, emailResults */
function initialize() {
__i();
}
function runQueries() {
__r();
}
function emailResults() {
__e();
}
(function (global) {
"use strict";
var DEBUG = !!global.__DEBUG;
function d(value, default_) {
return value != null ? value : default_;
}
function get(name) {
return d(global[name], defaults[name]);
}
var assert, assertType, writeLog, log, error, section;
if (DEBUG) {
assert = function (condition, message) {
if (!condition) throw new TypeError(message);
};
assertType = function (obj, type, message) {
assert(typeof obj === type, message);
};
var lastBlank = false;
writeLog = function (str) {
lastBlank = str === void 0;
Logger.log('== Timed Filters == ' + d(str, ''));
};
log = function (str) { writeLog('LOG: ' + str); };
error = function (str) { writeLog('ERROR: ' + str); };
section = function section(str) {
if (!lastBlank) writeLog();
writeLog(str);
writeLog();
};
} else {
assert = assertType = writeLog = log = error = section = function () {};
}
var defaults = {
'queries': null,
'queryInterval': 10,
'notifyEmail': true,
'subject': 'Weekly Deletion Totals',
'header': 'Number of emails successfully deleted this past week: {}',
'emailWeekday': 'Monday',
'emailHour': 1,
'email': null
};
var properties = PropertiesService.getUserProperties();
function requiredParameter(name) {
return '`' + name + '` is required for this script to function ' +
'properly. Please fix this as soon as possible.\n' +
'\n' +
'Documentation for this script can be found at ' +
'https://gist.github.com/isiahmeadows/63716b78c58b116c8eb7' +
'.';
}
var QUERIES = get('queries');
assert(QUERIES !== null, requiredParameter('queries'));
assert(Array.isArray(QUERIES), 'queries must be an array');
each(QUERIES, function (pair) {
assertType(pair[0], 'string', 'query must be a string');
assertType(pair[1], 'function', 'callback must be a function');
});
// If we don't get a lock within an hour, then there's something seriously
// wrong. This is for the less frequent runs.
var MAX_TIMEOUT = 1000 /*ms*/ * 60 /*s*/ * 60 /*min*/;
var USER = d(get('email'), Session.getEffectiveUser().getEmail());
assert(USER !== '' || global.email != null, requiredParameter('email'));
var SUBJECT = get('subject');
var HEADER = get('header');
assert(USER, 'there should exist a user email somewhere');
assertType(USER, 'string', 'email must be a string');
assertType(SUBJECT, 'string', 'subject must be a string');
assertType(HEADER, 'string', 'header must be a string');
var NOTIFY_EMAIL = get('notifyEmail');
assertType(NOTIFY_EMAIL, 'boolean',
'notifyEmail must be a boolean if given.');
// Shorthand, primarily to better assist in name mangling.
function exportName(name, fn) {
assertType(name, 'string', 'name must be a string');
assertType(fn, 'function', 'object must be a function');
fn.name = fn.displayName = name;
global['__' + name] = fn;
}
function doSync(block) {
var lock = LockService.getUserLock();
lock.waitLock(MAX_TIMEOUT);
var ret = block();
lock.releaseLock();
return ret;
}
function each(xs, f) {
for (var i = 0, len = xs.length; i !== len; i++) f(xs[i]);
}
function parseHour(hour) {
if (typeof hour === 'number') {
assert(hour % 1 === 0, 'emailHour must be an integer');
assert(hour >= 0 && hour < 24,
'emailHour is out of range (not within 0-23)');
return hour;
}
assertType(hour, 'string', 'emailHour must be a string or number');
hour = hour.trim();
var suffix = hour.slice(-2);
var last = suffix[1];
if (last === 'm') {
hour = hour.slice(0, -2);
suffix = suffix[0];
} else {
hour = hour.slice(0, -1);
suffix = suffix[1];
}
var res = parseInt(hour, 10);
if (res === 12) {
if (suffix === 'a') res = 0;
} else if (suffix === 'p') {
res += 12;
}
assert(res >= 0 && res < 24,
'emailHour is out of range (not within 12am-11pm)');
return res;
}
exportName('i', function initialize() {
section('Initializing triggers');
var day = get('emailWeekday');
assertType(day, 'string', 'emailWeekday should be a string.');
day = day.toUpperCase();
var hour = parseHour(get('emailHour'));
// The lock is needed to make sure the callbacks aren't executed while we
// are setting them up.
doSync(function () {
log('Removing duplicate triggers...');
each(ScriptApp.getProjectTriggers(), function (trigger) {
var handler = trigger.getHandlerFunction();
if (handler === 'runQueries' || handler === 'emailResults') {
log('Removing trigger for function: ' + trigger);
ScriptApp.deleteTrigger(handler);
}
});
log('Resetting total...');
properties.setProperty('total', '0');
log('Installing triggers...');
var queryInterval = get('queryInterval');
assertType(queryInterval, 'number', 'queryInterval must be a number');
ScriptApp.newTrigger('runQueries')
.timeBased()
.everyMinutes(queryInterval)
.create();
ScriptApp.newTrigger('emailResults')
.timeBased()
.everyWeeks(1)
.onWeekDay(ScriptApp.WeekDay[day])
.atHour(hour)
.create();
});
section('Triggers initialized successfully.');
});
exportName('r', function runQueries() {
section('Running queries');
var lock = LockService.getUserLock();
log('Scheduled run now commencing...');
if (!lock.tryLock(500)) {
// A single lock failure isn't the end of the world here. It's only
// going to try again a minute later.
error('Run failed. Gracefully exiting...');
return;
}
var total = +properties.getProperty('total');
each(QUERIES, function (pair) {
var query = pair[0];
var callback = pair[1];
log('Executing query: ' + query);
each(GmailApp.search(query), function (thread) {
log('Addressing Gmail thread: ' + thread.getFirstMessageSubject());
callback(thread);
total++;
});
});
properties.setProperty('total', total + '');
lock.releaseLock();
section('Queries sucessfully executed.');
});
exportName('e', function emailResults() {
section('Emailing results');
if (!NOTIFY_EMAIL) return;
log('Generating email...');
var total = doSync(function () {
log('Retrieving total...');
return properties.getProperty('total');
});
log('Total: ' + total);
log('Resetting total...');
properties.setProperty('total', '0');
log('Sending weekly email...');
var body = HEADER.replace(/\{\}/g, total);
log('Email: ' + USER);
log('Subject: ' + SUBJECT);
log('Body: ' + body);
GmailApp.sendEmail(USER, SUBJECT, body);
section('Email sent successfully');
});
section('Script initialized successfully.');
})(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment