需要帮助优化标记电子邮件的Google Apps脚本

时间:2013-03-05 21:52:44

标签: email gmail google-apps-script gmail-imap

Gmail存在一个问题,即会话标签未应用于到达会话线索的新邮件。 issue details here

我们found一个Google Apps脚本,用于修复Gmail收件箱中各个邮件的标签,以解决此问题。脚本如下:

function relabeller() {
  var labels = GmailApp.getUserLabels();


  for (var i = 0; i < labels.length; i++) {
    Logger.log("label: " + i + " " + labels[i].getName());

    var threads = labels[i].getThreads(0,100);
    for (var j = 1; threads.length > 0; j++) {
      Logger.log( (j - 1) * 100 + threads.length);
      labels[i].addToThreads(threads);
      threads = labels[i].getThreads(j*100, 100);
    }
  }
}

但是,由于Google Apps脚本的执行时间限制为5分钟,此脚本会在超过20,000条消息的电子邮箱上超时。

任何人都可以建议一种优化此脚本的方法,以便它不会超时吗?

3 个答案:

答案 0 :(得分:13)

好的,我已经为此工作了几天,因为我对Gmail标签/不会在对话中标记邮件的奇怪方式感到非常沮丧。

实际上,我很惊讶标签不会自动应用于对话中的新消息。这在Gmail用户界面中根本没有反映出来。无法查看线程并确定标签仅适用于线程中的某些消息,并且您无法在UI中向单个消息添加标签。当我在下面的脚本中工作时,我注意到您甚至无法以编程方式将标签添加到单个邮件中。因此,目前的行为确实没有理由。

随着我的咆哮,我有一些关于剧本的笔记。

  1. 我将Saqib的代码与Serge的代码结合起来。
  2. 该脚本包含两部分:初始运行,重新标记所有附加了用户标签的线程,以及标记最近电子邮件的维护运行(当前回溯4天)。一次运行期间只执行一个部件。初始运行完成后,只运行维护部件。您可以设置触发器,每天运行一次,或者或多或少地运行,具体取决于您的需要。
  3. 初始运行在4分钟后停止,以避免被5分钟的脚本时间限制终止。它设置一个触发器,在4分钟后再次运行(这两个时间都可以使用脚本中的常量进行更改)。触发器在下次运行时被删除。
    • 维护部分没有运行时检查。如果您在过去4天内收到大量电子邮件,则维护部分可能会达到脚本时间限制。我可能会在这里更改脚本以提高效率,但到目前为止它对我有用,所以我并没有真正想要改进它。
  4. 在初始运行中有一个try / catch语句试图捕获Gmail“写入配额错误”并正常退出(即写入当前进度以便稍后可以再次拾取),但我不知道是否它有效,因为我无法发生错误。
  5. 当达到时间限制时,以及初始运行完成时,您会收到一封电子邮件。
  6. 由于某种原因,即使使用Logger.clear()命令,日志也不会始终在运行之间完全清除。因此,它通过电子邮件发送给用户的状态日志不仅仅包含最新的运行信息。我不知道为什么会这样。
  7. 我用它来处理大约半小时内的20,000封电子邮件(包括等待时间)。我实际上运行了两次,所以它在一天内处理了40,000封电子邮件。我想Gmail的读/写限制为10,000不是这里应用的(可能一次将标签应用于100个线程计为单个写入事件而不是100?)。根据它发送的状态电子邮件,它在4分钟的运行中通过大约5,000个线程。

    很抱歉排长队。我责怪宽屏显示器。让我知道你的想法!

    function relabelGmail() {
    
      var startTime= (new Date()).getTime(); // Time at start of script
      var BATCH=100; // total number of threads to apply label to at once.
      var LOOKBACKDAYS=4; // Days to look back for maintenance section of script. Should be at least 2
      var MAX_RUN_TIME=4*60*1000; // Time in ms for max execution. 4 minutes is a good start.
      var WAIT_TIME=4*60*1000; // Time in ms to wait before starting the script again.
      Logger.clear();
    
    
    
    //  ScriptProperties.deleteAllProperties(); return; // Uncomment this line and run once to start over completely
    
      if(ScriptProperties.getKeys().length==0){ // this is to create keys on the first run
        ScriptProperties.setProperties({'itemsProcessed':0, 'initFinished':false, 'lastrun':'20000101', 'itemsProcessedToday':0, 
                                        'currentLabel':'null-label-NOTREAL', 'currentLabelStart':0, 'autoTrig':0, 'autoTrigID':'0'});
      }
    
      var itemsP = Number(ScriptProperties.getProperty('itemsProcessed')); // total counter
      var initTemp = ScriptProperties.getProperty('initFinished'); // keeps track of when initial run is finished. 
      var initF = (initTemp.toLowerCase() == 'true'); // Make it boolean
    
      var lastR = ScriptProperties.getProperty('lastrun'); // String of date corresponding to itemsProcessedToday in format yyyymmdd
      var itemsPT = Number(ScriptProperties.getProperty('itemsProcessedToday')); // daily counter
      var currentL = ScriptProperties.getProperty('currentLabel'); // Label currently being processed
      var currentLS = Number(ScriptProperties.getProperty('currentLabelStart')); // Thread number to start on
    
      var autoT = Number(ScriptProperties.getProperty('autoTrig')); // Number to say whether the last run made an automatic trigger
      var autoTID = ScriptProperties.getProperty('autoTrigID'); // Unique ID of last written auto trigger
    
      // First thing: google terminates scripts after 5 minutes. 
      // If 4 minutes have passed, this script will terminate, write some data, 
      // and create a trigger to re-schedule itself to start again in a few minutes. 
      // If an auto trigger was created last run, it is deleted here.
      if (autoT) {
        var allTriggers = ScriptApp.getProjectTriggers();
    
        // Loop over all triggers. If trigger isn't found, then it must have ben deleted.
        for(var i=0; i < allTriggers.length; i++) {
          if (allTriggers[i].getUniqueId() == autoTID) {
            // Found the trigger and now delete it
            ScriptApp.deleteTrigger(allTriggers[i]);
            break;
          }
        }
        autoT = 0;
        autoTID = '0';
      }
    
      var today = dateToStr_();
      if (today == lastR) { // If new day, reset daily counter
        // Don't do anything
      } else {
        itemsPT = 0;
      }
    
      if (!initF) { // Don't do any of this if the initial run has been completed
        var labels = GmailApp.getUserLabels();
    
        // Find position of last label attempted
        var curLnum=0;
        for ( ; curLnum < labels.length; curLnum++) { 
          if (labels[curLnum].getName() == currentL) {break};
        }
        if (curLnum == labels.length) { // If label isn't found, start over at the beginning
          curLnum = 0;
          currentLS = 0;
          itemsP=0;
          currentL=labels[0].getName();
        }
    
        // Now start working through the labels until the quota is hit.
        // Use a try/catch to stop execution if your quota has been hit. 
        // Google can actually automatically email you, but we need to clean up a bit before terminating the script so it can properly pick up again tomorrow.
        try {
          for (var i = curLnum; i < labels.length; i++) {
            currentL = labels[i].getName(); // Next label
            Logger.log('label: ' + i + ' ' + currentL);
    
            var threads = labels[i].getThreads(currentLS,BATCH);
    
            for (var j = Math.floor(currentLS/BATCH); threads.length > 0; j++) {
              var currTime = (new Date()).getTime();
              if (currTime-startTime > MAX_RUN_TIME) {
    
                // Make the auto-trigger
                autoT = 1; // So the auto trigger gets deleted next time.
    
                var autoTrigger = ScriptApp.newTrigger('relabelGmail')
                .timeBased()
                .at(new Date(currTime+WAIT_TIME))
                .create();
    
                autoTID = autoTrigger.getUniqueId();
    
                // Now write all the values.
                ScriptProperties.setProperties({'itemsProcessed':itemsP, 'initFinished':initF, 'lastrun':today, 'itemsProcessedToday':itemsPT, 
                                                'currentLabel':currentL, 'currentLabelStart':currentLS, 'autoTrig':autoT, 'autoTrigID':autoTID});
    
                // Send an email
                var emailAddress = Session.getActiveUser().getEmail();
                GmailApp.sendEmail(emailAddress, 'Relabel job in progress', 'Your Gmail Relabeller has halted to avoid termination due to excess ' +
                                   'run time. It will run again in ' + WAIT_TIME/1000/60 + ' minutes.\n\n' + itemsP + ' threads have been processed. ' + itemsPT + 
                                   ' have been processed today.\n\nSee the log below for more information:\n\n' + Logger.getLog());
                return;
              } else {
                // keep on going
                var len = threads.length;
                Logger.log( j * BATCH + len);
    
                labels[i].addToThreads(threads);
    
                currentLS = currentLS + len;
                itemsP = itemsP + len;
                itemsPT = itemsPT + len;
                threads = labels[i].getThreads( (j+1) * BATCH, BATCH);
              }
            }
    
            currentLS = 0; // Reset LS counter
          }
    
          initF = true; // Initial run is done
    
        } catch (e) { // Clean up and send off a notice. 
          // Write current values back to ScriptProperties
          ScriptProperties.setProperties({'itemsProcessed':itemsP, 'initFinished':initF, 'lastrun':today, 'itemsProcessedToday':itemsPT, 
                                          'currentLabel':currentL, 'currentLabelStart':currentLS, 'autoTrig':autoT, 'autoTrigID':autoTID});
    
          var emailAddress = Session.getActiveUser().getEmail();
          var errorDate = new Date();
          GmailApp.sendEmail(emailAddress, 'Error "' + e.name + '" in Google Apps Script', 'Your Gmail Relabeller has failed in the following stack:\n\n' + 
                             e.stack + '\nThis may be due to reaching your daily Gmail read/write quota. \nThe error message is: ' + 
                             e.message + '\nThe error occurred at the following date and time: ' + errorDate + '\n\nThus far, ' + 
                             itemsP + ' threads have been processed. ' + itemsPT + ' have been processed today. \nSee the log below for more information:' + 
                             '\n\n' + Logger.getLog());
          return;
        }
    
        // Write current values back to ScriptProperties. Send completion email.
        ScriptProperties.setProperties({'itemsProcessed':itemsP, 'initFinished':initF, 'lastrun':today, 'itemsProcessedToday':itemsPT, 
                                        'currentLabel':currentL, 'currentLabelStart':currentLS, 'autoTrig':autoT, 'autoTrigNumber':autoTID});
    
        var emailAddress = Session.getActiveUser().getEmail();
        GmailApp.sendEmail(emailAddress, 'Relabel job completed', 'Your Gmail Relabeller has finished its initial run.\n' + 
                           'If you continue to run the script, it will skip the initial run and instead relabel ' + 
                           'all emails from the previous ' + LOOKBACKDAYS + ' days.\n\n' + itemsP + ' threads were processed. ' + itemsPT + 
                           ' were processed today. \nSee the log below for more information:' + '\n\n' + Logger.getLog());
    
        return; // Don't run the maintenance section after initial run finish
    
      } // End initial run section statement
    
    
      // Below is the 'maintenance' section that will be run when the initial run is finished. It finds all new threads
      // (as defined by LOOKBACKDAYS) and applies any existing labels to all messages in each thread. Note that this 
      // won't miss older threads that are labeled by the user because all messages in a thread get the label
      // when the label action is first performed. If another message is then sent or received in that thread, 
      // then this maintenance section will find it because it will be deemed a "new" thread at that point. 
      // You may need to search further back the first time you run this if it took more than 3 days to finish
      // the initial run. For general maintenance, though, 4 days should be plenty.
    
      // Note that I have not implemented a script-run-time check for this section. 
    
      var threads = GmailApp.search('newer_than:' + LOOKBACKDAYS + 'd', 0, BATCH); // 
      var len = threads.length;
    
      for (var i=0; len > 0; i++) {
    
        for (var t = 0; t < len; t++) {
          var labels = threads[t].getLabels();
    
          for (var l = 0; l < labels.length; l++) { // Add each label to the thread
            labels[l].addToThread(threads[t]);
          }
        }
    
        itemsP = itemsP + len;
        itemsPT = itemsPT + len;
    
        threads = GmailApp.search('newer_than:' + LOOKBACKDAYS + 'd', (i+1) * BATCH, BATCH); 
        len = threads.length;
      }
      // Write the property data
      ScriptProperties.setProperties({'itemsProcessed':itemsP, 'initFinished':initF, 'lastrun':today, 'itemsProcessedToday':itemsPT, 
                                      'currentLabel':currentL, 'currentLabelStart':currentLS, 'autoTrig':autoT, 'autoTrigID':autoTID});
    }
    
    
    // Takes a date object and turns it into a string of form yyyymmdd
    function dateToStr_(dateObj) { //takes in a date object, but uses current date if not a date
    
      if (!(dateObj instanceof Date)) {
        dateObj = new Date();
      }
    
      var dd = dateObj.getDate();
      var mm = dateObj.getMonth()+1; //January is 0!
      var yyyy = dateObj.getFullYear();
    
      if(dd<10){dd='0'+dd}; 
      if(mm<10){mm='0'+mm};
      dateStr = ''+yyyy+mm+dd;
    
      return dateStr;
    
    }
    

    编辑:3/24/2017 我想我应该打开通知或其他东西,因为我从未见过来自user29020的问题。如果有人有同样的问题,这就是我做的事情:我通过设置每天晚上1点到凌晨2点之间的每日触发器来运行它作为维护功能。

    另外需要注意的是:似乎在过去一年左右的某些时候,对Gmail的标记调用显着放缓。现在每个线程需要大约0.2秒,所以我预计最初运行的20k电子邮件需要至少20次运行才能完成所有操作。这也意味着如果您通常每天收到超过100-200封电子邮件,维护部分也可能开始耗费太长时间并开始失败。现在这是很多电子邮件,但我敢打赌,有些人会接收到这么多的电子邮件,而且当我第一次写这篇邮件时,你似乎更有可能达到每天1000封左右的电子邮件。脚本。

    无论如何,一个缓解措施是将LOOKBACKDAYS减少到少于4个,但我不建议将它减少到少于2个。

答案 1 :(得分:5)

来自the documentation :

方法getInboxThreads()

检索所有收件箱主题,无论标签如何 当所有线程的大小太大而系统无法处理时,此调用将失败。如果线程大小未知且可能非常大,请使用'paged'调用,并指定每次调用中要检索的线程范围。 *

因此,您应该处理一定数量的线程,标记消息并设置时间触发器,每10分钟左右运行一次“页面”,直到所有消息都被标记。


编辑:我已经给了尝试,请考虑作为草稿开始:

该脚本将一次处理100个线程并向您发送电子邮件通知您其进度并显示日志。

完成后,它会通过电子邮件向您发出警告。它使用scriptProperties来存储其状态。 (不要忘记更新脚本末尾的邮件地址)。我尝试将时间触发器设置为5分钟,现在似乎运行顺利......

function inboxLabeller() {

  if(ScriptProperties.getKeys().length==0){ // this is to create keys on the first run
    ScriptProperties.setProperties({'threadStart':0, 'itemsprocessed':0, 'notF':true})
    }
    var items = Number(ScriptProperties.getProperty('itemsprocessed'));// total counter
    var tStart = Number(ScriptProperties.getProperty('threadStart'));// the value to start with
    var notFinished = ScriptProperties.getProperty('notF');// the "main switch" ;-)
    Logger.clear()

  while (notFinished){ // the main loop
    var threads = GmailApp.getInboxThreads(tStart,100);
    Logger.log('Number of threads='+Number(tStart+threads.length));
      if(threads.length==0){
      notFinished=false ;
      break
      }
      for(t=0;t<threads.length;++t){
       var mCount = threads[t].getMessageCount();
       var mSubject = threads[t].getFirstMessageSubject();
       var labels = threads[t].getLabels();
       var labelsNames = '';
         for(var l in labels){labelsNames+=labels[l].getName()}
       Logger.log('subject '+mSubject+' has '+mCount+' msgs with labels '+labelsNames)
         for(var l in labels){
             labels[l].addToThread(threads[t])
      }
      }
        tStart = tStart+100;
        items = items+100
        ScriptProperties.setProperties({'threadStart':tStart, 'itemsprocessed':items})
        break
      }
   if(notFinished){
      GmailApp.sendEmail('mymail', 'inboxLabeller progress report', 'Still working, '+items+' processed \n - see logger below \n \n'+Logger.getLog());
      }else{
      GmailApp.sendEmail('mymail', 'inboxLabeller End report', 'Job completed : '+items+' processed');
      ScriptProperties.setProperties({'threadStart':0, 'itemsprocessed':0, 'notF':true})
      }
}

答案 2 :(得分:1)

这将找到没有标签的单个邮件,并应用关联线程的标签。它花费的时间要少得多,因为它不会重新标记每条消息。

function label_unlabeled_messages() {
  var unlabeled = GmailApp.search("has:nouserlabels -label:inbox -label:sent -label:chats -label:draft -label:spam -label:trash");

  for (var i = 0; i < unlabeled.length; i++) {
    Logger.log("thread: " + i + " " + unlabeled[i].getFirstMessageSubject());
    labels = unlabeled[i].getLabels();
    for (var j = 0; j < labels.length; j++) {
      Logger.log("labels: " + i + " " + labels[j].getName());
      labels[j].addToThread(unlabeled[i]);
    }
  }
}