Подтвердить что ты не робот

Нужна помощь в оптимизации приложений Google script, которые подписывают электронные письма

У Gmail есть проблема, когда метки ярлыков не применяются к новым сообщениям, которые поступают в цепочку бесед. подробнее здесь

Мы нашли приложение Google Script, которое исправляет ярлыки отдельных сообщений в папке "Входящие" Gmail для решения этой проблемы. Script выглядит следующим образом:

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

Однако этот Script не работает в почтовых ящиках с более чем 20 000 сообщений из-за ограничения времени выполнения 5 минут в Google Apps Script.

Может кто-нибудь предложить способ оптимизации этого Script, чтобы он не перегревался?

4b9b3361

Ответ 1

Хорошо, я работаю над этим несколько дней, потому что меня очень расстроило странным образом, что Gmail называет/не помещает сообщения в разговорах.

Я действительно ошеломлен тем, что ярлыки автоматически не применяются к новым сообщениям в разговоре. Это совсем не отражено в пользовательском интерфейсе Gmail. Нет способа взглянуть на поток и определить, что метки применяются только к некоторым сообщениям в потоке, и вы не можете добавлять метки к одному сообщению в пользовательском интерфейсе. Когда я работал над моим script ниже, я заметил, что вы даже не можете программно добавлять метки к одному сообщению. Таким образом, на самом деле нет причин для текущего поведения.

С моей разглагольностью, у меня есть несколько заметок о script.

  • Я вроде комбинировал код Saqib с кодом Сержа.
  • script состоит из двух частей: начального запуска, который переносит все потоки с прикрепленной меткой пользователя и прогон обслуживания, который маркирует последние электронные письма (в настоящее время оглядывается назад на 4 дня). Выполняется только одна часть во время одного прогона. Как только начальный запуск будет завершен, будет выполняться только часть обслуживания. Вы можете установить триггер, чтобы он запускался один раз в день или более или менее часто, в зависимости от ваших потребностей.
  • Начальный запуск останавливается через 4 минуты, чтобы не прерываться с помощью 5-минутного ограничения времени script. Он запускает триггер снова через 4 минуты (оба эти значения можно изменить с помощью констант в script). Триггер будет удален при следующем запуске.
    • В разделе обслуживания нет проверки времени выполнения. Если за последние 4 дня у вас много писем, раздел обслуживания может попасть в ограничение времени script. Я мог бы, вероятно, изменить script, чтобы быть более эффективным здесь, но до сих пор это работало для меня, поэтому я не очень мотивирован, чтобы улучшить его.
  • В первом прогоне есть инструкция try/catch, чтобы попытаться поймать "ошибку квоты на запись Gmail" и выйти изящно (т.е. записать текущий прогресс, чтобы его можно было забрать позже), но я не знаю, если он работает, потому что я не мог получить ошибку.
  • Вы получите электронное письмо, когда достигнут срок, и когда начальный запуск завершен.
  • По какой-то причине журнал не всегда полностью очищается между прогонами даже при использовании команды Logger.clear(). Таким образом, журналы состояния, которые он отправляет электронной почте пользователю, имеют больше, чем просто самую последнюю информацию о запуске. Я не знаю, почему это происходит.

Я использовал это для обработки 20 000 писем примерно за полчаса (включая время ожидания). Я фактически запускал его дважды, поэтому он обработал 40 000 писем за один день. Я полагаю, что предел чтения/записи Gmail в 10 000 не является тем, что здесь применяется (возможно, при применении метки к 100 потокам за один раз вместо одиночного события записи вместо 100?). Он пропускает около 5000 потоков в течение 4 минут, в соответствии с отправленным им адресом статуса.

Извините за длинные строки. Я обвиняю широкоформатные мониторы. Дайте мне знать, что вы думаете!

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 секунды на поток, поэтому я ожидаю, что первоначальный запуск 20-килограммовых электронных писем займет не менее 20 прогонов до того, как он пройдет весь путь. Это также означает, что если вы обычно получаете более 100-200 писем в день, раздел обслуживания может также занять слишком много времени и начать сбой. Теперь, когда много писем, но я готов поспорить, есть некоторые люди, которые получают это много, и кажется гораздо более вероятным, что вы ударите его, чем 1000 или около того ежедневных писем, которые были бы необходимы для отказа, когда я впервые написал script.

В любом случае, одним из смягчений будет уменьшение LOOKBACKDAYS до менее 4, но я бы не рекомендовал помещать его меньше 2.

Ответ 2

Из документации:

метод getInboxThreads()

Получить все темы Inbox независимо от ярлыков Этот вызов завершится неудачно, если размер всех потоков слишком большой для обработки системы. Если размер потока неизвестен и потенциально очень большой, используйте вызов "paged" и укажите диапазоны потоков для извлечения в каждом вызове. *

Таким образом, вы должны обрабатывать определенное количество потоков, помечать сообщения и настраивать триггер времени для запуска каждой "страницы" каждые 10 минут или около того, пока все сообщения не будут помечены.


РЕДАКТИРОВАТЬ: я дал этому try, пожалуйста, рассмотрите в качестве черновика, чтобы начать с:

script будет обрабатывать 100 потоков одновременно и отправлять вам электронное письмо, чтобы информировать вас о его прогрессе и показывать журнал.

Когда он будет завершен, он также предупредит вас по электронной почте. Он использует scriptProperties для хранения своего состояния. (не забудьте обновить почтовый адрес в конце script). Я попробовал это с триггером времени, установленным на 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})
      }
}

Ответ 3

Это найдет отдельные сообщения, у которых нет метки, и примените метку связанного потока. Это занимает гораздо меньше времени, потому что не переписывает каждое сообщение.

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