Search code examples
google-apps-scriptgoogle-drive-apigmail

Gmail to Google Drive script broken


I'm not sure if there has been a recent update with Google App Script or not, but I've been using this script for a few months and it is now broken and not working.

Here's the script I am using:

Code.gs:

// Gmail2GDrive
// https://github.com/ahochsteger/gmail2gdrive

/**
 * Returns the label with the given name or creates it if not existing.
 */
function getOrCreateLabel(labelName) {
  var label = GmailApp.getUserLabelByName(labelName);
  if (label == null) {
    label = GmailApp.createLabel(labelName);
  }
  return label;
}

/**
 * Recursive function to create and return a complete folder path.
 */
function getOrCreateSubFolder(baseFolder,folderArray) {
  if (folderArray.length == 0) {
    return baseFolder;
  }
  var nextFolderName = folderArray.shift();
  var nextFolder = null;
  var folders = baseFolder.getFolders();
  while (folders.hasNext()) {
    var folder = folders.next();
    if (folder.getName() == nextFolderName) {
      nextFolder = folder;
      break;
    }
  }
  if (nextFolder == null) {
    // Folder does not exist - create it.
    nextFolder = baseFolder.createFolder(nextFolderName);
  }
  return getOrCreateSubFolder(nextFolder,folderArray);
}

/**
 * Returns the GDrive folder with the given path.
 */
function getFolderByPath(path) {
  var parts = path.split("/");

  if (parts[0] == '') parts.shift(); // Did path start at root, '/'?

  var folder = DriveApp.getRootFolder();
  for (var i = 0; i < parts.length; i++) {
    var result = folder.getFoldersByName(parts[i]);
    if (result.hasNext()) {
      folder = result.next();
    } else {
      throw new Error( "folder not found." );
    }
  }
  return folder;
}

/**
 * Returns the GDrive folder with the given name or creates it if not existing.
 */
function getOrCreateFolder(folderName) {
  var folder;
  try {
    folder = getFolderByPath(folderName);
  } catch(e) {
    var folderArray = folderName.split("/");
    folder = getOrCreateSubFolder(DriveApp.getRootFolder(), folderArray);
  }
  return folder;
}






/**
 * Processes a message
 */
function processMessage(message, rule, config) {
  Logger.log("INFO:       Processing message: "+message.getSubject() + " (" + message.getId() + ")");
  var messageDate = message.getDate();
  var attachments = message.getAttachments();

  for (var attIdx=0; attIdx<attachments.length; attIdx++) {
    var attachment = attachments[attIdx];
    var attachmentName = attachment.getName();

    Logger.log("INFO:         Processing attachment: "+attachment.getName());
    var match = true;
    if (rule.filenameFromRegexp) {
    var re = new RegExp(rule.filenameFromRegexp);
      match = (attachment.getName()).match(re);
    }
    if (!match) {
      Logger.log("INFO:           Rejecting file '" + attachment.getName() + " not matching" + rule.filenameFromRegexp);
      continue;
    }
    try {
      var folder = getOrCreateFolder(Utilities.formatDate(messageDate, config.timezone, rule.folder));


     /////////////////////////////////////////////////////////////////////////////////////////////

      // var file = folder.removeFile(attachment);
      // file.setContent(attachment);


      var fileName = attachment.getName();
      var f = folder.getFilesByName(fileName);
      var file = f.hasNext() ? f.next() : folder.createFile(attachment);

      // file.setContent(attachment);

      /////////////////////////////////////////////////////////////////////////////////////////////



      if (rule.filenameFrom && rule.filenameTo && rule.filenameFrom == file.getName()) {
        var newFilename = Utilities.formatDate(messageDate, config.timezone, rule.filenameTo.replace('%s',message.getSubject()));
        Logger.log("INFO:           Renaming matched file '" + file.getName() + "' -> '" + newFilename + "'");
        file.setName(newFilename);
      }
      else if (rule.filenameTo) {
        var newFilename = Utilities.formatDate(messageDate, config.timezone, rule.filenameTo.replace('%s',message.getSubject()));
        Logger.log("INFO:           Renaming '" + file.getName() + "' -> '" + newFilename + "'");
        file.setName(newFilename);
      }
      file.setDescription("Mail title: " + message.getSubject() + "\nMail date: " + message.getDate() + "\nMail link: https://mail.google.com/mail/u/0/#inbox/" + message.getId());
      Utilities.sleep(config.sleepTime);
    } catch (e) {
      Logger.log(e);
    }
  }
}

/**
 * Generate HTML code for one message of a thread.
 */
function processThreadToHtml(thread) {
  Logger.log("INFO:   Generating HTML code of thread '" + thread.getFirstMessageSubject() + "'");
  var messages = thread.getMessages();
  var html = "";
  for (var msgIdx=0; msgIdx<messages.length; msgIdx++) {
    var message = messages[msgIdx];
    html += "From: " + message.getFrom() + "<br />\n";
    html += "To: " + message.getTo() + "<br />\n";
    html += "Date: " + message.getDate() + "<br />\n";
    html += "Subject: " + message.getSubject() + "<br />\n";
    html += "<hr />\n";
    html += message.getBody() + "\n";
    html += "<hr />\n";
  }
  return html;
}

/**
* Generate a PDF document for the whole thread using HTML from .
 */
function processThreadToPdf(thread, rule, html) {
  Logger.log("INFO: Saving PDF copy of thread '" + thread.getFirstMessageSubject() + "'");
  var folder = getOrCreateFolder(rule.folder);
  var html = processThreadToHtml(thread);
  var blob = Utilities.newBlob(html, 'text/html');
  var pdf = folder.createFile(blob.getAs('application/pdf')).setName(thread.getFirstMessageSubject() + ".pdf");
  return pdf;
}

/**
 * Main function that processes Gmail attachments and stores them in Google Drive.
 * Use this as trigger function for periodic execution.
 */
function Gmail2GDrive() {
  if (!GmailApp) return; // Skip script execution if GMail is currently not available (yes this happens from time to time and triggers spam emails!)
  var config = getGmail2GDriveConfig();
  var label = getOrCreateLabel(config.processedLabel);
  var end, start;
  start = new Date(); // Start timer

  Logger.log("INFO: Starting mail attachment processing.");
  if (config.globalFilter===undefined) {
    config.globalFilter = "has:attachment -in:trash -in:drafts -in:spam";
  }

  // Iterate over all rules:
  for (var ruleIdx=0; ruleIdx<config.rules.length; ruleIdx++) {
    var rule = config.rules[ruleIdx];
    var gSearchExp  = config.globalFilter + " " + rule.filter + " -label:" + config.processedLabel;
    if (config.newerThan != "") {
      gSearchExp += " newer_than:" + config.newerThan;
    }
    var doArchive = rule.archive == true;
    var doPDF = rule.saveThreadPDF == true;

    // Process all threads matching the search expression:
    var threads = GmailApp.search(gSearchExp);
    Logger.log("INFO:   Processing rule: "+gSearchExp);
    for (var threadIdx=0; threadIdx<threads.length; threadIdx++) {
      var thread = threads[threadIdx];
      end = new Date();
      var runTime = (end.getTime() - start.getTime())/1000;
      Logger.log("INFO:     Processing thread: "+thread.getFirstMessageSubject() + " (runtime: " + runTime + "s/" + config.maxRuntime + "s)");
      if (runTime >= config.maxRuntime) {
        Logger.log("WARNING: Self terminating script after " + runTime + "s")
        return;
      }

      // Process all messages of a thread:
      var messages = thread.getMessages();
      for (var msgIdx=0; msgIdx<messages.length; msgIdx++) {
        var message = messages[msgIdx];
        processMessage(message, rule, config);
      }
      if (doPDF) { // Generate a PDF document of a thread:
        processThreadToPdf(thread, rule);
      }

      // Mark a thread as processed:
     thread.addLabel(label);

      if (doArchive) { // Archive a thread if required
        Logger.log("INFO:     Archiving thread '" + thread.getFirstMessageSubject() + "' ...");
        thread.moveToArchive();
      }
    }
  }
  end = new Date(); // Stop timer
  var runTime = (end.getTime() - start.getTime())/1000;
  Logger.log("INFO: Finished mail attachment processing after " + runTime + "s");
}

Config.gs:

/**
 * Configuration for Gmail2GDrive
 * See https://github.com/ahochsteger/gmail2gdrive/blob/master/README.md for a config reference
 */
function getGmail2GDriveConfig() {
  return {
    // Global filter
    "globalFilter": "-in:trash -in:drafts -in:spam",
    // Gmail label for processed threads (will be created, if not existing):
    "processedLabel": "to-gdrive/processed",
    // Sleep time in milli seconds between processed messages:
    "sleepTime": 100,
    // Maximum script runtime in seconds (google scripts will be killed after 5 minutes):
    "maxRuntime": 45,
    // Only process message newer than (leave empty for no restriction; use d, m and y for day, month and year):
    "newerThan": "1m",
    // Timezone for date/time operations:
    "timezone": "GMT",

    // Processing rules:
    "rules": [
      /* { // Store all attachments sent to [email protected] to the folder "Scans"
        "filter": "has:attachment to:[email protected]",
        "folder": "'Scans'-yyyy-MM-dd"
      },
      { // Store all attachments from [email protected] to the folder "Examples/example1"
        "filter": "has:attachment from:[email protected]",
        "folder": "'Examples/example1'"
      }, */


      { // Store all pdf attachments from [email protected] to the folder "Examples/example2"
        "filter": "label:gmail2drive",
        "folder": "'Swann'",
        "filenameFromRegexp": ".*\.jpg$",
        "archive": true
      },


      // { // Store all attachments from [email protected] OR from:[email protected]
        // to the folder "Examples/example3ab" while renaming all attachments to the pattern
        // defined in 'filenameTo' and archive the thread.
        // "filter": "has:attachment (from:[email protected] OR from:[email protected])",
        // "folder": "'Examples/example3ab'",
        // "filenameTo": "'file-'yyyy-MM-dd-'%s.txt'",
        // "archive": true
      // },

      /* {
        // Store threads marked with label "PDF" in the folder "PDF Emails" als PDF document.
        "filter": "label:PDF",
        "saveThreadPDF": true,
        "folder": "PDF Emails"
      },
      { // Store all attachments named "file.txt" from [email protected] to the
        // folder "Examples/example4" and rename the attachment to the pattern
        // defined in 'filenameTo' and archive the thread.
        "filter": "has:attachment from:[email protected]",
        "folder": "'Examples/example4'",
        "filenameFrom": "file.txt",
        "filenameTo": "'file-'yyyy-MM-dd-'%s.txt'"
      } */

    ]
  };
}

Essentially, the script checks for emails with the label "gmail2drive" and if it exists, extracts the attachments in the email and uploads it to a folder called "Swann" in my Google Drive. Then it applies the label "to-gdrive/processed" to the processed emails, so they don't get processed again.

Occasionally, some attachments may be extracted twice, creating duplicates. So the script also checks for duplicates as well and hopefully prevents that from happening.

So this has been working fine for the most part, but recently it broke, resulting in the issue where the same attachments get extracted multiple times, and the same emails get processed multiple times. It's like the script ignores the label "to-gdrive/processed" or something.

I have tried using different labels and the result is the same.

I should also clarify that I am not a programmer or a scripting guy. I know just very very little in how to get this set up in Google Script. I can follow general instructions OK. I'm hoping for somebody who knows how to read scripts be able to troubleshoot this and let me know what to change.

Thanks in advance.


Solution

  • After many comments and a chat conversation, we deduced that this issue is produced by the DVR new emails being treated as replies of a thread. This was messing with the labels and causing to repeat uploads to the Drive folder.

    New script:

    Assuming there is a rule in the inbox that attaches the label "gmail2drive" to the threads received from the DVR, this gets all the threads with that label, processes each message of each thread and re-sends it as a new email. It also changes the subject with a Date (including milliseconds), so it makes sure each new email has a different subject and is independent from the others. After processing all the messages, sends the thread to the Bin.

    function OrganiseEmails() {
    
      var threads = GmailApp.search("-in:trash -in:drafts -in:spam label:gmail2drive -label:to-gdrive-processed")
    
      for (i in threads){
        var messages = threads[i].getMessages();
    
        for (j in messages){
          if (messages[j].getAttachments().length > 0){  
            var to = messages[j].getTo();
            var date = Utilities.formatDate(new Date(), "GMT","yyyy-MM-dd' at 'HH:mm:ss:SS' '");
            var subject = "DVR motion detected - " + date;
            var body = "test";
            var attachment = messages[j].getAttachments()[0];
    
            var options = {
              attachments: attachment
            }
            GmailApp.sendEmail(to, subject, body, options);
          }      
        }
        var rem_label = GmailApp.getUserLabelByName("gmail2drive"); 
        rem_label.removeFromThread(threads[i]);
        threads[i].moveToTrash();
      }  
    }
    

    I made some changes to the Gmail2Drive and Config.gs script as now it's not going to filter by label but by subject:

    Code.gs

    1. Removed the function getOrCreateLabel(). We are not using a filter in this script so it's not necessary.
    2. Removed the line var label = getOrCreateLabel(config.processedLabel); for obivous reasons.
    3. Removed + " -label:" + config.processedLabel from the gSearchExp variable declaration
    4. Changed the line thread.addLabel(label); to thread.moveToTrash();

    Config.gs

    1. Added in:inbox to the globalFilter search parameters to avoid threads being processed again because now they are also in Sent.
    2. Removed the line "processedLabel": "to-gdrive/processed",
    3. Changed the "filter" from "label:gmail2drive" to "subject:'DVR motion detected - '"

    EDIT

    Finally we decided to put both codes together, so create a new function in the code.gs and call it first in the Gmail2Drive function. Also, instead of changing labels you can delete the emails permanently with Gmail.Users.Threads.remove("me", threadid); after sending the emails.