2
votes

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.

1
I cannot replicate the issue, for me it works correctly and doesn't repeat uploads. Even if I manually remove the to-gdrive/processed label it still does nothing as the file is in the Drive folder. I see that other users experienced this issue with the application. Since this was reported a few days ago and it works fine for me, could you confirm if you still have this problem? - Jescanellas
Hi, thanks for your reply. I tried it just now and it's still broken for me. Let's say you have 5 emails and 3 of them have already been processed. You delete the attachments for the 3 processed emails from Google Drive. If you run the Google App Script, it should only process 2 emails and extract 2 attachments into your Google Drive. However, it goes through the previous 3 emails and extracts a total of 5 attachments into your Google Drive. - shadowz1337
I'm sorry, I followed the steps one by one and still worked. Created the 5 emails with attachments, processed 3 first, worked fine, removed the files from drive and then I processed the other 2. It worked as expected. To make sure I removed the 2 files from Drive and ran the script again. Nothing was processed. The only thing I changed from your code is the name folder and the file extension (txt). - Jescanellas
Hmm that's odd. Does it work for you using .jpg attachments? Could you replicate the issue using the exact scripts and use images instead of .txt files? - shadowz1337
Massive thanks to Jescanellas for his help and his patience. Very much appreciated. My updated script now looks like this: Code.gs: pastebin.com/az0N59qC Config.gs: pastebin.com/vt0CcDfX - shadowz1337

1 Answers

5
votes

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.