gmail add-on

111 views
Skip to first unread message

gsuiter

unread,
Mar 29, 2024, 2:38:59 PMMar 29
to Google Apps Script Community
I have created the following gmail add-on and am looking for some guidance. The goal is to enable the inbox owner to select several mails and package them into a zip file. The owner can then download or drag and drop the URL to another application. I am facing two problems. The add-on view seems to only be additive for each new email selected. Selecting a previously selected email only shows buttons for emails / files that were selected and created prior. Also, once a file has been removed, it cant be added again. Its as if each mail has its own view or cache of the add-on and I cant figure out how to refresh the view during each event or mail selection.

Also, in addition to the zip button, I want to have a html text view of the URL in the card but cant figure out the refresh sequence to show it, it is currently commented out.

code:

// Function to load the Gmail add-on
function loadAddOn(event) {
  var url = ""
  if (event) {
    var accessToken = event.gmail.accessToken;
    var messageId = event.gmail.messageId;
    GmailApp.setCurrentMessageAccessToken(accessToken);
    var mailMessage = GmailApp.getMessageById(messageId);
    var eml = mailMessage.getRawContent();
    var date = new Date().getTime(); // Get current epoch time

    // Create or get the folder where emails will be stored
    var tempFolder;
    try {
      tempFolder = DriveApp.getFoldersByName('temp').next();
    } catch (e) {
      tempFolder = DriveApp.createFolder('temp');
    }

    // Create a file for the current email in the folder with a unique name
    var messageIdSuffix = messageId.split(':').pop(); // Get the part after the colon
    var fileName = mailMessage.getSubject() + '_' + messageIdSuffix + '.eml'; // Use message ID suffix in the file name
    var emailFile = createUniqueFile(tempFolder, fileName, eml);
  }

  // Retrieve all files in the folder if it exists
  var emailButtons = [];
  if (tempFolder) {
    var files = tempFolder.getFiles();
    // Create buttons for each email file in the folder
    while (files.hasNext()) {
      var file = files.next();
      if (!file.isTrashed() && file.getName().indexOf('.zip') === -1) { // Check if file is not .zip
        var emailId = file.getId();
        var emailSubject = file.getName().replace(/\.eml$/, ''); // Remove extension
        var button = CardService.newTextButton()
          .setText(emailSubject)
          .setOnClickAction(CardService.newAction()
            .setFunctionName('handleEmailSelection')
            .setParameters({ 'emailId': emailId }));
        emailButtons.push(button);
      }
    }
  }

  // Add a button to remove all emails from the folder
  var clearAllButton = CardService.newTextButton()
    .setText('Clear All')
    .setOnClickAction(CardService.newAction().setFunctionName('clearAllEmails'));

  var buttonSet = CardService.newButtonSet();
  for (var i = 0; i < emailButtons.length; i++) {
    buttonSet.addButton(emailButtons[i]);
  }
  buttonSet.addButton(clearAllButton);

  // Check if there are files to create a zip file
  var hasFiles = emailButtons.length > 0;

  // Add a button to produce the zip file when there are files
  if (hasFiles) {
    var zipButton = CardService.newTextButton()
      .setText('Create Zip File')
      .setOnClickAction(CardService.newAction().setFunctionName('createZipFile'));
    buttonSet.addButton(zipButton);
  }

  // Create a section with the button set
  var section = CardService.newCardSection().addWidget(buttonSet);

  // Create a card with the section
  var card = CardService.newCardBuilder()
    .setHeader(CardService.newCardHeader().setTitle('Selected Emails'))
    .addSection(section)
    .build();

  return [card];
}

// Function to create a file with a unique name in the given folder
function createUniqueFile(folder, fileName, content) {
  var existingFiles = folder.getFilesByName(fileName);
  if (existingFiles.hasNext()) {
    // If a file with the same name already exists, return that file
    return existingFiles.next();
  } else {
    // Otherwise, create a new file with the unique name
    return folder.createFile(fileName, content);
  }
}

// Function to handle email selection
function handleEmailSelection(e) {
  // Retrieve the selected email ID from the event parameters
  var emailId = e.parameters.emailId;

  // Get the file by ID and delete it
  var file = DriveApp.getFileById(emailId);
  file.setTrashed(true);

  // Reload the add-on card after handling email selection
  return loadAddOn();
}

// Function to remove all emails from the folder
function clearAllEmails() {
  var tempFolder = DriveApp.getFoldersByName('temp').next();
  var files = tempFolder.getFiles();
  while (files.hasNext()) {
    var file = files.next();
    file.setTrashed(true);
  }

  // Reload the add-on card after clearing all emails
  return loadAddOn();
}

// Function to create a zip file
function createZipFile() {
  var tempFolder = DriveApp.getFoldersByName('temp').next();
  var files = tempFolder.getFiles();
  var blobs = [];
  while (files.hasNext()) {
    blobs.push(files.next().getBlob());
  }
  var zipBlob = Utilities.zip(blobs, 'selected_emails.zip');
  var zipFile = tempFolder.createFile(zipBlob);
  var url = "https://drive.google.com/uc?export=download&id=" + zipFile.getId();
  return CardService.newActionResponseBuilder()
    .setNotification(CardService.newNotification()
      .setType(CardService.NotificationType.INFO)
      .setText('Zip file created.'))
    .setOpenLink(CardService.newOpenLink().setUrl(url))
    //.addSection(CardService.newCardSection()
    //.addWidget(CardService.newTextParagraph().setText("Click <a href="+ url +">here</a> to download .zip")))
    .build();
}

// Entry point for the add-on
function buildAddOn(e) {
  return loadAddOn(e);
}

gsuiter

unread,
Apr 2, 2024, 3:05:35 PMApr 2
to Google Apps Script Community
I have made some progress with this but it still does not function well. There are several issues:

1. Once a file has been removed using the button, that email can not be added back as a button or file.
2. After deleting a file, the card updates itself automatically but for the card to update after selecting an email, i must manually click the refresh button.
3. The URL button and hyperlink both take you to a new browser window but dont download the file, you must refresh that browser tab or request the url again for it to actually initiate a download, maybe thats due to google wrapping URL's in its own request service?

I would really appreciate some eyes and feedback on this. Thank you.

// Function to create the homepage card with a list of files from the Google Drive folder
function createHomepageCard() {
  var card = CardService.newCardBuilder()
    .setHeader(CardService.newCardHeader()
      .setTitle('Files in Google Drive Folder')
    )
    .addSection(createFileListWidget())
    .addSection(CardService.newCardSection().addWidget(createRefreshButton()))
    .build();

  return card;
}

// Function to create a widget displaying the list of files from the Google Drive folder
function createFileListWidget() {
  var folder;
  try {
    folder = DriveApp.getFoldersByName('temp').next(); // Get the folder named "temp"
  } catch (e) {
    // If the folder is missing, create it
    folder = DriveApp.createFolder('temp');
  }

  var files = folder.getFiles();
  var buttonSet = CardService.newButtonSet();

  var fileCount = 0;
  var zipFileBlob;

  while (files.hasNext()) {
    var file = files.next();
    var fileName = file.getName();
    var fileId = file.getId();
    if (!fileName.endsWith('.zip')) {
      var fileNameParts = fileName.split('_'); // Split the filename by "_"
      var buttonName = fileNameParts.slice(0, -1).join("_"); // Join all parts except the last one
      var button = CardService.newTextButton()
        .setText(buttonName) // Set the button name to only display the part before the last "_"
        .setOnClickAction(CardService.newAction()
          .setFunctionName('deleteFile')
          .setParameters({ fileId: fileId })
        );
      buttonSet.addButton(button);
    }
    else {
      zipFileBlob = fileId; // Save the file ID of the zip file
    }
    fileCount++;
  }

  // Add a "Clear All" button if there are files present
  if (fileCount > 0) {
    buttonSet.addButton(CardService.newTextButton()
      .setText('Clear All')
      .setOnClickAction(CardService.newAction()
        .setFunctionName('clearAllFiles')
      )
    );
  }

  // Add a "Refresh" button to go back to the root card
  //buttonSet.addButton(createRefreshButton());

  // Add a "Zip Files" button if there are files present
  if (fileCount > 0) {
    buttonSet.addButton(CardService.newTextButton()
      .setText('Zip Files')
      .setOnClickAction(CardService.newAction()
        .setFunctionName('zipFiles')
      )
    );
  }

  var section;
  if (fileCount > 0) {
    section = CardService.newCardSection().addWidget(buttonSet);
    if (zipFileBlob) {

    var downloadWidgets = createDownloadLink(zipFileBlob); // Using the modified createDownloadLink function

    // Extracting the paragraph and hyperlink widgets from the returned object
    var paragraph = downloadWidgets.paragraph;
    var hyperlink = downloadWidgets.hyperlink;

    // Adding the new CardSection containing both the paragraph and hyperlink to the main section
    section.addWidget(hyperlink);
    section.addWidget(paragraph);

    }
  } else {
    section = CardService.newCardSection().addWidget(
      CardService.newTextParagraph().setText('No files found. Please select an email to add files to the queue.')
    );
  }

  return section;
}

// Function to create a hyperlink for the zip file download
function createDownloadLink(blob) {
  var url;
  if (typeof blob === 'string') {
    // If the input is already a file ID, use it directly
  } else if (blob.getId) {
    // If the input is a Blob object, get its ID
  } else {
    throw new Error('Invalid input for createDownloadLink function.');
  }
 
  // Construct HTML string for the link
  var html = "Click <a href='" + url + "'>here</a> to download .zip";

  // Create a paragraph widget with the HTML
  var paragraph = CardService.newTextParagraph().setText(html);

  var hyperlink = CardService.newTextButton()
    .setText('Download Zip File')
    .setOpenLink(CardService.newOpenLink().setUrl(url));
 
  // Return an object containing both paragraph and hyperlink
  return {
    paragraph: paragraph,
    hyperlink: hyperlink
  };
  }

// Function to create a zip file and provide a link to Google Drive
function createZipFile() {
  var folder = DriveApp.getFoldersByName('temp').next(); // Get the folder named "temp"

  // Check if there's an existing zip file and delete it
  var existingZipFiles = folder.getFilesByName('selected_files.zip');
  while (existingZipFiles.hasNext()) {
    var existingZipFile = existingZipFiles.next();
    existingZipFile.setTrashed(true);
  }

  var files = folder.getFiles();
  var blobs = [];
  while (files.hasNext()) {
    var file = files.next();
    if (!file.getName().endsWith('.zip')) {
      blobs.push(file.getBlob());
    }
  }
  var zipBlob = Utilities.zip(blobs, 'selected_files.zip');
  var zipFile = folder.createFile(zipBlob);

  // Refresh the card after creating the zip file
  var updatedCard = createHomepageCard();
  return CardService.newActionResponseBuilder()
    .setNavigation(CardService.newNavigation().updateCard(updatedCard))
    .setNotification(CardService.newNotification()
      .setType(CardService.NotificationType.INFO)
      .setText('Zip file created.'))
    .build();
}

// Function to handle zipping files when the "Zip Files" button is clicked
function zipFiles() {
  return createZipFile();
}

// Function to handle deleting a file when its button is clicked
function deleteFile(e) {
  var fileId = e.parameters.fileId;
  var file = DriveApp.getFileById(fileId);
  file.setTrashed(true);

  // Delete any existing zip file
  deleteExistingZipFile();

  // Refresh the card after deleting the file
  var updatedCard = createHomepageCard();
  return CardService.newActionResponseBuilder()
    .setNavigation(CardService.newNavigation().updateCard(updatedCard))
    .build();
}

// Function to handle clearing all files when the "Clear All" button is clicked
function clearAllFiles() {
  var folder = DriveApp.getFoldersByName('temp').next(); // Get the folder named "temp"
  var files = folder.getFiles();

  while (files.hasNext()) {
    var file = files.next();
    file.setTrashed(true);
  }

  // Refresh the card after clearing all files
  var updatedCard = createHomepageCard();
  return CardService.newActionResponseBuilder()
    .setNavigation(CardService.newNavigation().updateCard(updatedCard))
    .build();
}

// Function to go back to the root card (refresh)
function gotoRootCard() {
  var nav = CardService.newNavigation()
    .popToRoot()
    .updateCard(onHomepage());
 
  return CardService.newActionResponseBuilder()
    .setNavigation(nav)
    .build();        
}

// Function to create a refresh button
function createRefreshButton() {
  return CardService.newTextButton()
    .setText('Refresh')
    .setOnClickAction(CardService.newAction()
      .setFunctionName('gotoRootCard')
    );
}

// Function to handle the event when an email is selected in the inbox
function onEmailSelected(event) {
  var messageId = event.gmail.messageId;
  var mailMessage = GmailApp.getMessageById(messageId);
  var subject = mailMessage.getSubject();
  var body = mailMessage.getRawContent();
  var messageIdSuffix = messageId.split(':').pop(); // Get the part after the colon

  // Check if a file with the same name already exists
  var folder = DriveApp.getFoldersByName('temp').next(); // Get the folder named "temp"
  var existingFiles = folder.getFilesByName(subject + "_" + messageIdSuffix + ".eml");

  // If a file with the same name already exists, skip the creation process
  if (existingFiles.hasNext()) {
    return;
  }

  // Create a .eml file in the Google Drive directory
  var emlFile = folder.createFile(subject + "_" + messageIdSuffix + ".eml", body);

  // Refresh the homepage card to display the new file button
  return CardService.newActionResponseBuilder()
    .setStateChanged(true) // Indicate that the state of the card has changed
    .setNavigation(CardService.newNavigation().popToRoot()) // Navigate back to the root card
    .build();
}

// Function to handle the event when the add-on is loaded
function onHomepage(e) {
  return createHomepageCard();
}

// Entry point for the add-on
function buildAddOn(e) {
  var accessToken = e.messageMetadata.accessToken;
  var authorized = GmailApp.getUserInfo(accessToken).isCurrentUser;

  if (!authorized) {
    throw new Error('Access denied: Not authorized.');
  }

  // Set up event handler for email selection
  GmailApp.setCurrentMessageAccessToken(accessToken);
  return createHomepageCard();
}

// Function to delete any existing zip file
function deleteExistingZipFile() {
  var folder = DriveApp.getFoldersByName('temp').next(); // Get the folder named "temp"
  var existingZipFiles = folder.getFilesByName('selected_files.zip');
  while (existingZipFiles.hasNext()) {
    var existingZipFile = existingZipFiles.next();
    existingZipFile.setTrashed(true);
  }
}


Ed Robinson

unread,
Apr 7, 2024, 7:33:10 PMApr 7
to Google Apps Script Community
Hi Gsuiter,
probably the reason you're not getting a ton of help is the complexity for people in the community to set up the project themselves in their own environments and see whats going on.

I wonder if it is possible to create a shareable spreadsheet/document/project that demonstrates the issue?

Reply all
Reply to author
Forward
0 new messages