2013-03-05 43 views
11

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); 
    } 
    } 
} 

但是这个脚本超时的电子邮箱,与超过20,000的消息,由于在谷歌Apps脚本的5分钟执行时间限制。

任何人都可以请建议一种方法来优化这个脚本,使其不超时?

+0

如何运行此脚本?你如何设置自动运行? – 2016-02-17 17:47:52

+0

问题详细信息链接获取403错误。 – 2016-02-17 17:52:55

回答

13

好了,我一直在这几天因为我的怪很沮丧Gmail标签/不会在会话中标记邮件的方式。

实际上,我很惊讶地发现标签不会自动应用于对话中的新消息。这在Gmail用户界面中完全没有体现出来。没有办法查看线程并确定标签仅适用于线程中的某些消息,并且不能将标签添加到UI中的单个消息。当我正在处理下面的脚本时,我注意到,您甚至无法以编程方式将标签添加到单条消息中。所以目前的行为确实没有理由。

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

  1. 我把Saqib的代码和Serge的代码结合起来。
  2. 该脚本有两个部分:初始运行,重新映射附加了用户标签的所有线程,以及标注最近电子邮件的维护运行(当前回看4天)。在一次运行中只有一个部件执行。一旦初始运行完成,只有维护部分将运行。您可以将触发器设置为每天运行一次,或者多次或多次运行,具体取决于您的需要。
  3. 4分钟后初始运行暂停,以避免被5分钟脚本时间限制终止。它将触发器设置为在4分钟后再次运行(这两个时间可以使用脚本中的常量进行更改)。触发器在下次运行时被删除。
    • 有一个在维护部分没有运行时检查。如果您在过去4天内有大量电子邮件,维护部分可能会遇到脚本时间限制。我可以在这里更改脚本以提高效率,但到目前为止它已经适用于我,所以我没有真正的动力去改进它。
  4. 有一个在初始运行一个try/catch语句,试图赶上Gmail的“写配额错误”并退出优雅(即写入目前的进度,因此它可以在以后再次回升),但我不”不知道它是否有效,因为我不能发生错误。
  5. 你会得到的是到达时间限制时,电子邮件,以及在初始运行结束。
  6. 出于某种原因,该日志不总是完全运行之间清晰,使用Logger.clear()命令时也是如此。因此,它通过电子邮件发送给用户的状态日志不仅仅是最新的运行信息。我不知道为什么会发生这种情况。

我已经在大约半个小时(包括等待时间)中使用它来处理20,000封电子邮件。我实际上跑了两次,所以它在一天内处理了4万封电子邮件。我猜Gmail的读写限制不是这里所应用的(可能一次将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; 

} 

编辑:2017年3月24日 我想我应该打开通知什么的,因为我从来没有看到从user29020的问题。如果有人遇到同样的问题,我就是这样做的:我将它作为维护功能运行,方法是设置每天1到2点之间的每天夜间触发器。

附加的注释:看来,在过去一年左右的某一点,标签调用到Gmail已显著放缓。现在每个线程大约需要0.2秒,所以我希望最初的20k邮件运行至少需要20次左右才能完成。这也意味着,如果您通常每天收到超过100至200封电子邮件,维护部分可能会开始花费太长时间并开始失败。现在有很多电子邮件,但我敢打赌,有一些人收到了很多电子邮件,而且看起来更有可能是这样,而不是每天发送1000封左右的每日电子邮件脚本。

无论如何,一个减缓将降低LOOKBACKDAYS小于4,但我不建议把它小于2

+0

这实际上帮助我编写一个基于Apps脚本的应用程序,在Gmail中自己实现缺失的功能(即无法按组筛选)。 – 2014-01-24 00:29:11

+0

您提到这将在init之后作为“维护”功能运行。有没有办法让我的Gmail邮件在后台定期运行?到目前为止,我只能弄清楚如何仅从script.google.com运行您的漂亮脚本。任何帮助或指针将不胜感激 - 谢谢! – Kalin 2015-02-25 01:17:31

+0

我刚刚跑过这个脚本,看着日志,似乎只有用户标签受到影响。是否可以使用系统标签(如带星号或重要标签)进行此项工作? – adamlogan 2017-04-25 22:50:33

5

the documentation :

方法getInboxThreads()

检索所有的收件箱线程,不论标签 当所有线程的尺寸过大的系统来处理此调用将失败。如果线程大小未知,并且可能非常大,请使用“分页”调用,并指定要在每次调用中检索的线程范围。 *

所以,你应该处理一定数量的线程,标签的邮件,并成立了时间触发运行每个“页”每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}) 
     } 
} 
+0

谢谢塞尔。你能推荐一些代码来完成你的建议吗? – 2013-03-06 00:12:58

+0

Serge。感谢您发布优化的代码。过去几天我一直在运行它,但问题是我每天都会触发Gmail写入配额。不幸的是,Google Apps脚本配额看起来非常严格。任何其他想法? – 2013-03-18 21:13:38

+3

此代码对我的解决方案有帮助。需要注意的一点是,由于此代码使用getInboxThreads来查找线程,因此它不会处理任何存档线程。 – GordonM 2013-03-28 19:57:14

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]); 
    } 
    } 
} 
+0

只选择未标记的消息(以及他们的对话)来采取行动是一个整洁的想法。此方法是否仍将新邮件的标签修复为已由过滤器分配了用户标签的旧对话?假设我们有一个过滤器将所有传入消息分配为标签“stdlabel”。用户打开一个对话并将其标记为“商业”(对话的消息现在都有两个标签)。然后一个新的答复进来;过滤器将其指定为“stdlabel”。这个脚本是否将这个新消息修复为“stdlabel”和“business”?谢谢! – Kalin 2015-02-24 19:47:53

+1

@ user29020不,因为它在开始任何工作之前过滤到具有“nouserlabels”的消息。 “stdlabel”将被视为用户标签,因此这些电子邮件将不会被选中。 – janpio 2015-04-24 12:18:24