Please help me improve this Google Apps Script for reminding the user to contact people they haven't contacted in a while:Purpose:Creates tasks for you to contact people you haven't talked to for a while.Adds a 'Touch Frequency (Days)' field to contacts - make a schedule per ContactNew emails to/from a contact with make Task go away automaticallyComplete the task manually to signify a touch over a different channel (Phone, Chat, In-person)Known issues:'Touch Frequency (Days)' field not appearing in Google Contacts interfaceSporadic execution errors Data Storage Error, Backend Error - not sure why.  But try it again and it works.Tasks are duplicated with each run - major issue - should we also search by task title as a backup if the task is missing (in case the DB was wiped out)?Slower and more reads than necessary, perhaps?Code// http://code.google.com/p/touchminder//// Run TouchMinder() on a scheduled trigger as a Google Apps Script.// You'll need to add an API Key for your Tasks Service integration (Easy: https://developers.google.com/apps-script/service_tasks, https://developers.google.com/console/help/)//// Creates tasks for you to contact people you haven't talked to for a while.// Adds a 'Touch Frequency (Days)' field to contacts - make a schedule per Contact// New emails to/from a contact with make Task go away automatically// Complete the task manually to signify a touch over a different channel (Phone, Chat, In-person)//// Known issues:// 'Touch Frequency (Days)' field not appearing// Currently, this doesn't fully work.  Sporadic execution errors Data Storage Error, Backend Error - not sure why.  But try it again and it works.// Tasks are duplicated with each run - major issue// Slower and more reads than necessary, perhaps?function TouchMinder() {  Logger.log('Starting TouchMinder');  Logger.log('');  var db = ScriptDb.getMyDb(); // Get the db  // Cursor to [] of contactTouches ({ table: 'ContactTouches', contactId: 0, lastTouch: 0, taskId: 0 })  var contactTouches = [];     var queryResults = db.query({ table: 'ContactTouches' });  while(queryResults.hasNext()) { contactTouches.push(queryResults.next()); }  // Who am I?  var currentUserEmailAddress = Session.getActiveUser().getEmail();  var myId = ContactsApp.getContact(currentUserEmailAddress).getId();  var allCurrentUserEmailAddresses = ContactsApp    .getContact(currentUserEmailAddress)    .getEmails()    .map(function (ea) { return ea.getAddress().trim().toLowerCase(); });  // Cursor to [] of contactTouches ({ table: 'ContactTouches', ownerContactId: myId, contactId: contactId, lastTouch: number, taskId: taskId }) // Cannot store Date, so store milliseconds instead  // ownerContactId because I believe the scriptDb is shared by the domain in Google Apps accounts.  If not, no real harm, right?.  var contactTouches = [];     var queryResults = db.query({ table: 'ContactTouches', ownerContactId: myId });  while(queryResults.hasNext()) { contactTouches.push(queryResults.next()); }  Logger.log('currentUserEmailAddresses: ' + allCurrentUserEmailAddresses.join(', '));  // load all contacts  var trackedContacts = loadContacts(contactTouches, myId);  var maxDays = Math.max.apply( Math, trackedContacts.map(function (c) { return c.frequency; }) ) || 0; // What's the furthest back we should search?  var earliestDate = new Date(); earliestDate.setDate(earliestDate.getDate() - maxDays);  Logger.log('Found ' + trackedContacts.length + ' trackedContacts');  Logger.log('');  var tasklistId = getTouchMinderTaskList();  var processedTasks = processCompletedTasksGetRemaining(tasklistId, db); // { completed: number, remaining: taskId[] }  Logger.log('Touched ' + processedTasks.completed + ' contacts from completed tasks.');  Logger.log('');  Logger.log('Searching back as far as ' + earliestDate.toLocaleString());  Logger.log('');  var messages = new IterateMessages('in:anywhere -in:drafts'); // Iterate through ALL emails, sent & received.  while (messages.moveNext()) {    try {      var message = messages.current.message;      var thread = messages.current.thread;      if (thread.getLastMessageDate() < earliestDate) break;      var sender = extractRawEmailAddresses(message.getFrom())[0];      var recipients = extractRawEmailAddresses(message.getTo() + ',' + message.getCc());      var date = message.getDate().getTime(); // number!      if (allCurrentUserEmailAddresses.filter(function (myEmail) { return sender === myEmail; }).length !== 0) { // Sent email                for (var r in recipients) {          var recipient = recipients[r];          var matchingContacts = trackedContacts.filter(function (tc) { // find matching contacts where the lastTouch is older than this date AND there's an email match            return tc.contactTouch.lastTouch < date &&               0 < tc.allEmails.filter(function (ea) { return ea === recipient }).length; });          for (var mc in matchingContacts) // update the lastTouch          {            matchingContacts[mc].contactTouch.lastTouch = date;          }        }      } else { // Received email        var matchingContacts = trackedContacts.filter(function (tc) {  // find matching contacts where the lastTouch is older than this date AND there's an email match          return tc.contactTouch.lastTouch < date &&             0 < tc.allEmails.filter(function (ea) { return ea === sender }).length; });        for (var mc in matchingContacts) // update the lastTouch        {          matchingContacts[mc].contactTouch.lastTouch = date;        }      }    } catch (ex) {        Logger.log('Error ' + ex + ' ' + message.getSubject());        throw ex;    }  }  Logger.log('Finished scanning');  // For which tasks to remove  var allRecentlyEmailed = trackedContacts.filter(function (tc) {     return tc.cutoffDate < tc.contactTouch.lastTouch &&  // lastTouch is after the cutoff for this contact      tc.contactTouch.taskId != null; // and the taskId is not null  });  // For which tasks to add  var allNewlyExpired = trackedContacts.filter(function (tc) {     return tc.contactTouch.lastTouch <= tc.cutoffDate && // lastTouch is before the cutoff for this contact      (tc.contactTouch.taskId == null || // and there is no taskId        processedTasks.remaining.filter(function (r) { return r === tc.contactTouch.taskId }).length === 0); // or the task is missing  });  for (var re in allRecentlyEmailed)  {    // delete the task    var recentlyEmailed = allRecentlyEmailed[re];    var task = processedTasks.remaining.filter(function (r) { return r.getId() === recentlyEmailed.contactTouch.taskId; })[0];    if (task)    {      task.setDeleted(true);      Tasks.Tasks.update(task, tasklistId, recentlyEmailed.contactTouch.taskId);    }    recentlyEmailed.contactTouch.taskId = null;  }  Logger.log('Deleted ' + allRecentlyEmailed.length + ' tasks');  for (var ne in allNewlyExpired)  {    // add a task    var newlyExpired = allNewlyExpired[ne];    var title = 'Check in on ' + newlyExpired.contact.getFullName();    Logger.log('Adding task ' + title + '');    var newTask = Tasks.newTask().setTitle(title);    newlyExpired.contactTouch.taskId = Tasks.Tasks.insert(newTask, tasklistId).getId();  }  Logger.log('Added ' + allNewlyExpired.length + ' tasks');  db.saveBatch(contactTouches, false);};// From and To lines in email are each presented as a raw string - we need to extract just the email address for matching to Contact email addresses.// This takes a string of comma or semi-colon delimited email addresses (including optionally quotes full-names preceeding brackets addresses, or just naked email addresses)// and returns an array of normalized addressed.function extractRawEmailAddresses(addressLine){  var reEmail = /\s*(?:([^<,;]+)|(?:(?:(?:(?:(?:|[^<,;])*)|[^<,;]*)?\s*<([^<>,;]*)>)))(?:$|,|;)/gi;  var extracted = [];  var captures;  while (captures = reEmail.exec(addressLine))  {    extracted.push((captures[2] || captures[1]).trim().toLowerCase());  }  return extracted;}// Ensure that the TaskList exists and return the ID.function getTouchMinderTaskList(){  var title = TouchMinders;  var allLists = Tasks.Tasklists.list().getItems();  var id;  for (var i in allLists) {    if (title == allLists[i].getTitle()) {      return allLists[i].getId();    }  }  var newTaskList = Tasks.newTaskList();  newTaskList.setTitle(title);  return Tasks.Tasklists.insert(newTaskList).getId();}// Look for manually Completed Tasks and interpret that to mean that the Contact was touched by the user through another channel (Phone, Chat, in-person)function processCompletedTasksGetRemaining(tasklistId, contactTouches, myId){  var tasks = Tasks.Tasks.list(tasklistId).getItems();  var completed = 0;  var remaining = [];  for (var t in tasks) {    var task = tasks[t];    var taskId = task.getId();    var completion = task.getCompleted();    if (completion)    {      var found = contactTouches.filter(function (ct) { return ct.taskId == taskId })[0];      if (found) {        var lastTouch = new Date(completion.replace(/\-/g,'\/').replace(/[T|Z]/g,' ')).getTime();        found.lastTouch = found.lastTouch < lastTouch ? lastTouch : found.lastTouch;        found.taskId = null;        task.setDeleted(true);        Tasks.Tasks.update(task, tasklistId, taskId);        completed++;      }      else      {        remaining.push(taskId);      }    }  }  return { completed: completed, remaining: remaining };}// Get all contacts from ContactsApp Google Service and pair them with cached data found in contactTouches ScriptDBfunction loadContacts(contactTouches, myId){  // My Contacts only - perhaps later this is configurable?  return ContactsApp.getContactGroup(System Group: My Contacts).getContacts()    .map(function (c) {      var touchFrequency = c.getCustomFields('Touch Frequency (Days)'); // Get the Contact's preferred Touch Frequency in Days as configured in the Google Contacts App custom field      if (!touchFrequency) { // if there is no such setting, create one and set it to a default of 30 days        touchFrequency = '30';        c.addCustomField('Touch Frequency (Days)', touchFrequency);      }      var contactId = c.getId();      var contactTouch = contactTouches.filter(function (ct) { return ct.contactId === contactId; })[0]; // Find the matching contactTouch      if (!contactTouch) // if it doesn't exist, add it to the array (it will be saved to the ScriptDB at the end of the entire script execution)      {        contactTouch = { table: 'ContactTouches', ownerContactId: myId, contactId: contactId, lastTouch: 0, taskId: null };        contactTouches.push(contactTouch);      }      var frequency = parseInt(touchFrequency, 10) || 30; // Attempt to parse the touchFrequency string as an int or default to 30 days      var cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - frequency); // Calculate the earliest date to search for touches with this contact      return {         frequency: frequency,         cutoffDate: cutoffDate.getTime(),        contact: c,        contactId: contactId,        primaryEmail: c.getPrimaryEmail,        allEmails: c.getEmails().map(function (ea) { return ea.getAddress().trim().toLowerCase(); }),        contactTouch: contactTouch      };    });}/*// IterateMessages created with the help of the Tracuer project from this source:function* IterateMessages(searchCriteria, backwardFromDate) {  var oldestYetObserved = (backwardFromDate || new Date()).getTime()/1000 | 0;  while(true)  {    // Older seems non-inclusive, which is desirable for this purpose, otherwise we'd loop infinitely on any single result    var batchSearchCriteria = searchCriteria + ' older:' + oldestYetObserved.toString();     var threads = GmailApp.search(batchSearchCriteria, 0, 10);    if (threads.length === 0) break;    for (var i = 0; i < threads.length; i++) {      var messages = threads[i].getMessages();      for (var j = 0; j < messages.length; j++) {        yield messages[j];      }    }    oldestYetObserved = threads[threads.length - 1].getLastMessageDate().getTime()/1000 | 0;  }};for (var message in IterateMessages('in:anywhere')){  console.log(message);}*/// Lazily iterates through email messages.  // Optionally pass any search criteria other than 'older:' for searchCriteria ex: 'in:inbox from:(john.doe@internet.com) subject:(Lunch?) has:attachment'.// Optionally pass a Date from which to iterate backwards in time (by each Thread's most recent email date)// Refer to the commented ECMAScript Harmony version above for basic detailsfunction IterateMessages(searchCriteria, backwardFromDate) {  var $that = this;  var $arguments = arguments;  var $state = 20;  var $storedException;  var $finallyFallThrough;  var batchSearchCriteria;  var i;  var j;  var messages;  var threadMessages;  var oldestYetObserved;  var threads;  var $result = {moveNext: function($yieldSent) {      while (true) try {        switch ($state) {          case 20:            oldestYetObserved = (backwardFromDate || new Date()).getTime() / 1000 | 0;            $state = 21;            break;          case 21:            if (true) {              $state = 10;              break;            } else {              $state = 19;              break;            }          case 10:            batchSearchCriteria = (searchCriteria || '') + ' older:' + oldestYetObserved.toString();            $state = 11;            break;          case 11:            threads = GmailApp.search(batchSearchCriteria, 0, 50);            messages = GmailApp.getMessagesForThreads(threads);            $state = 13;            break;          case 13:            if (messages.length === 0) {              $state = 14;              break;            } else {              $state = 15;              break;            }          case 14:            $state = 19;            break;          case 15:            i = 0;            $state = 7;            break;          case 7:            if (i < messages.length) {              $state = 5;              break;            } else {              $state = 9;              break;            }          case 4:            i++;            $state = 7;            break;          case 5:            threadMessages = messages[i];            $state = 6;            break;          case 6:            j = 0;            $state = 2;            break;          case 2:            if (j < threadMessages.length) {              $state = 0;              break;            } else {              $state = 4;              break;            }          case 1:            j++;            $state = 2;            break;          case 0:            $result.current = { thread: threads[i], message: threadMessages[j] };            $state = 1;            return true;          case 9:            oldestYetObserved = threads[threads.length - 1].getLastMessageDate().getTime() / 1000 | 0;            $state = 21;            break;          case 19:            $state = 23;          case 23:            return false;          case 22:            throw $storedException;          default:            throw invalid state in state machine  + $state;        }      } catch ($caughtException) {        $storedException = $caughtException;        switch ($state) {          default:            throw $storedException;        }      }    }};  return $result;}// Quickly reset the taskId for all ContactTouchesfunction reset() {  var currentUserEmailAddress = Session.getActiveUser().getEmail();  var myId = ContactsApp.getContact(currentUserEmailAddress).getId();  var contactTouches = [];    var db = ScriptDb.getMyDb();    var queryResults = db.query({ table: 'ContactTouches', ownerContactId: myId });  while(queryResults.hasNext()) { var next = queryResults.next(); next.taskId = null; contactTouches.push(next); }  db.saveBatch(contactTouches, false);}// Slowly delete the ScriptDB items one at a time (might affect all domain users!)function deleteAll() {   var db = ScriptDb.getMyDb();   while (true) {     var result = db.query({});      if (result.getSize() == 0) {       break;     }     while (result.hasNext()) {       db.remove(result.next());     }   }}
Google Apps Email, Contacts, Tasks, ScriptDB services integration
email;google apps script;google contacts api