6
\$\begingroup\$

Background

This is a function that is part of a library written in GAS. A function inside a library I've written is supposed to send out an email to another user or users with accompanying details (based on parameters) from the email account of whoever runs the function. Basically if I run the program from my google account, and your email address is one of the parameters, then you get an email from me. I realize that this is wrapped inside of Google's sendEmail function anyway, but I've added some of my own parameters.

What the function does specifically: when it is run, an email is sent to the specified email address/addresses with the accompanying subject and email body. Additionally, another email address can be added as a CC. Finally there is another option where, if the container the app script is attached to is a spreadsheet, you can specify which sheet on the spreadsheet you want converted into a PDF (specifying its PDF name), and have that sent with the email as well. Inside the function there can be six possible parameters: emailAddress, emailSubject, emailBody, emailCC, sheetName, and pdfName. I believe all of those are fairly self-explanatory besides the last two. As I previously mentioned, sheetName is the name of the sheet on the spreadsheet that you want converted into a PDF, and pdfName is the name you set your newly created PDF to.

My Request

I'm still somewhat new to GAS/JavaScript, and I wanted to challenge myself by making a library that contains a lot of the functions that my company uses on a regular basis so that I could learn more. Unfortunately, I'm very limited on who I can ask for guidance on how to improve my library. One of the methods that I was told about that was easier was to make separate functions for each of the combos of parameters within a top level object, but I have not been able to figure out how to properly implement that, so any assistance is appreciated.

My Code

function sendEmail(){
  var function1 = function(emailAddress, emailSubject, emailBody){
    MailApp.sendEmail(emailAddress, emailSubject, emailBody)
  }

  var function2 = function(emailAddress, emailSubject, emailBody, emailCC){
    MailApp.sendEmail(emailAddress, emailSubject, emailBody, {cc: emailCC})
  }

  var function3 = function(emailAddress, emailSubject, emailBody, sheetName, pdfName){
    MailApp.sendEmail(emailAddress, emailSubject, emailBody, {attachments: [pdfConversionPortrait(sheetName).setName(pdfName)]})
  }

  var function4 = function(emailAddress, emailSubject, emailBody, emailCC, sheetName, pdfName){
    MailApp.sendEmail(emailAddress, emailSubject, emailBody, {
      cc: emailCC,
      attachments: [pdfConversionPortrait(sheetName).setName(pdfName)]})
  }

  try{
    if(arguments.length === 3){
      function1(arguments[0].toString(), arguments[1], arguments[2])
    } else if(arguments.length === 4){
      function2(arguments[0].toString(), arguments[1], arguments[2], arguments[3].toString())
    } else if(arguments.length === 5){
      function3(arguments[0].toString(), arguments[1], arguments[2], arguments[3], arguments[4])
    } else if(arguments.length === 6){
      function4(arguments[0].toString, arguments[1], arguments[2], arguments[3].toString, arguments[4], arguments[5])
    }
  } catch(e){
    e instanceof TypeError ? Logger.log('Error: Specified sheet does not exist') : Logger.log(e)
  }
}

Other Relevant Code

function pdfConversionPortrait(sheetName){
  let ss = SpreadsheetApp.getActiveSpreadsheet()
  let ss_id = ss.getId()
  let sheet_id = ss.getSheetByName(sheetName).getSheetId()

  let response = UrlFetchApp.fetch("https://docs.google.com/spreadsheets/d/" + ss_id + "/export?format=pdf&gid=" + sheet_id, {
    muteHttpExceptions: true,
    headers: {
      Authorization: 'Bearer ' + ScriptApp.getOAuthToken(),
    },
  }).getBlob()
  return response  
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Welcome to Code Review! There is a beginner tag- you could add it if you'd like. \$\endgroup\$ Commented Jan 26, 2023 at 19:11
  • 1
    \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ Thank you, did not know that was a thing, so that's very helpful already! \$\endgroup\$ Commented Jan 26, 2023 at 20:00

2 Answers 2

2
\$\begingroup\$

Disclaimer

This is all untested code, written just based on reading the documentation.

If there's any inaccuracies, please just comment below and I will fix them.



Instead of having a ton of functions, just to limit the number of arguments, how about you accept an object with the arguments?

I'm going to base my answer in the MailApp.sendEmail(object) variant.
I've based the argument names from Eric Koleda's answer in StackOverflow, regarding optional fields.

It's convention to use the prefix "opt_" for optional parameters, but it's not required.

Using the variant that takes a single object allows you to have much cleaner code, and you can still set all the options that you want, with better input validation.

This also removes the need for 4 weird functions that do the exact same thing: pass values to MailApp.sendEmail().


Here's a simple example I've written:

function sendEmail(toEmail, subject, body, opt_cc, opt_sheetName, opt_pdfName){
  try {
    var options = {
      to: toEmail,
      subject: subject,
      body: body,
      cc: null,
      attachments: []
    }

    // If opt_cc was provided and isn't an empty string, add it
    if(opt_cc) {
      options.cc = opt_cc
    }
    
    // Adds the file if both opt_sheetName and opt_pdfName are provided
    if(opt_sheetName && opt_pdfName) {
      options.attachments = [
        pdfConversionPortrait(sheetName).setName(pdfName)
      ]
    } else if(opt_pdfName || opt_sheetName) {
      // If just one of them is provided, throws an exception
      throw "Sheet name and PDF name are required to attach a PDF file"
    }
    
    MailApp.sendEmail(options)
  } catch(e){
    e instanceof TypeError ? Logger.log('Error: Specified sheet does not exist') : Logger.log(e)
  }
}

I took the liberty of simplifying the names of the arguments, to make them easier to read.


If the functionality to allow sending the CC information is a must, based on arguments, you can do like this:

function sendEmail(){
  try {
    var options = {
      to: arguments[0],
      subject: arguments[1],
      body: arguments[2]
    }

    // Both have the 4th argument as the CC
    if(arguments.length === 4 || arguments.length >= 6) {
      options.cc = arguments[3]
    }

    // 5 or more arguments have the sheet name and pdf name as arguments
    if(arguments.length >= 5) {
      /*
        For 5 arguments, get the 4th and 5th arguments.
        For 6 or more arguments, get the 5th and 6th arguments.
      */
      var index = arguments.length === 5 ? 3 : 4;
      
      options.attachments = [
        pdfConversionPortrait(arguments[index])
          .setName(arguments[index + 1])
      ]
    }
    
    MailApp.sendEmail(options)
  } catch(e){
    e instanceof TypeError ? Logger.log('Error: Specified sheet does not exist') : Logger.log(e)
  }
}

It should have the same functionality as the 4 functions you've shown.


Something I would also change is this line:

e instanceof TypeError ? Logger.log('Error: Specified sheet does not exist') : Logger.log(e)

I would rewrite it to this:

Logger.log(e instanceof TypeError ? e : 'Error: Specified sheet does not exist')

It's a little bit shorter and aligns a little bit better with how one would use the ternary operator: to pass values based on a condition, instead of controlling the code execution (that's the job of an if).


Additionally, I would only wrap the pdfConversionPortrait(sheetName).setName(pdfName) in the try{ } catch block.

\$\endgroup\$
10
  • \$\begingroup\$ In the way you’ve set this up, if I provided the sheet name and the PDF name, but not the email CC, would this still work properly? \$\endgroup\$ Commented Jan 30, 2023 at 15:40
  • \$\begingroup\$ @LoftonGentry Yes, it should work, as one doesn't rely on the other. They are independent steps. You do have to pass null to opt_cc. \$\endgroup\$ Commented Jan 30, 2023 at 19:03
  • \$\begingroup\$ Okay thank you. I think this is a good starting point for what I’m trying to do. Do you know if there’s a way so that I wouldn’t have to pass null to opt_cc but still be able to send the sheet as PDF? Because that’s mainly one of the things I’ve been trying to figure out. I was told this is possible by having a top-level object and then placing functions within it, but I’m still unsure on how to do that \$\endgroup\$ Commented Jan 30, 2023 at 21:42
  • 1
    \$\begingroup\$ That is exactly what I was looking for. I was thinking about using arguments inside to object, but wasn't 100% confident on how to do so, so thank you, this dramatically improved the library and really helped further my understanding \$\endgroup\$ Commented Feb 1, 2023 at 16:59
  • 1
    \$\begingroup\$ @LoftonGentry Once you've finalized the implementation, put it on for a review. Either me or someone more knowledgeable will check it. \$\endgroup\$ Commented Feb 1, 2023 at 18:40
0
\$\begingroup\$

After some time has passed, I've slowly refined my code using the initial answer from Ismael Miguel as a basis, but also using other techniques I've used from other answers I've gotten on other posts.

What I Changed

It's been a little while since I last looked at the old version of this code, but it still utilizes objects. I wanted to use an object as a function parameter, much like how Google's MailApp.sendEmail() function has an advanced parameters function. I was able to successfully figure out how to do so, and rather than having opt_sheetName and opt_pdfName as parameters, they are now key-value pairs inside the object parameter. I will refer to the object parameter of the function as the PDF object now. Additionally, I added a third key-value pair, type, to the PDF object, which allows the user to decide if they want their PDF in landscape or portrait mode. Originally, determining if you wanted the PDF in landscape or portrait mode was not an option, and there was a whole separate function for sending the email in landscape mode. That is no longer the case after adding type to the PDF object.


Example of the old format with all parameters used:

sendEmail('an email', 'subject', 'body', 'cc'd email', 'sheetName', 'pdfName')

Example of new format with all parameters used:

sendEmail('an email', 'subject', 'body', 'cc'd email', {sheet: 'sheet1', name: 'pdfName', type: 'landscape'})


Below is a more detailed description of each of the parameters that will be fed into the new version of the sendEmail() function

Required Parameters:

  • to: This is the email or array of emails that the email will be initially sent to
  • subject: self-explanatory, this is the subject of the email that will be sent
  • body: Also self-explanatory, this is the body of the email that will be sent

Optional Parameters:

  • cc: The email or array of emails that will be cc'd onto the email that will be sent

  • Object PDF: The object PDF has three parameters: sheet, name, and type. sheet is the name of the sheet that will be converted into a PDF and attached to the email to be sent. name is the name that the PDF will be set to when it is created. type is the format the the PDF will be in when it is created. There are only two options for this, portrait and landscape.


Some Things to Note:

  • cc is not required if you want to add a PDF object as one of the parameters.

  • Example: sendEmail('an email', 'subject', 'body', {sheet: 'sheet1', name: 'pdfName', type: 'landscape'})

  • If a PDF object is set as a parameter, then sheet MUST be set to a valid sheet on the spreadsheet the code is attached to. However, name and type can be blank, as there are default values that are set if those values are left blank. The default value for name will just be the name of the sheet that is being converted into a PDF, and the default type will be in portrait format.

  • Example: sendEmail('an email', 'subject', 'body', {sheet: 'sheet1'})


My Code

function sendEmail(){
  let spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
  try{
    const opt = {to: arguments[0], subject: arguments[1], body: arguments[2] }

    if(arguments.length >= 4){
      //Check if fourth argument is an email or not
      validateEmail(arguments[3]) ? opt.cc = arguments[3] : null
      /*
        NOTE: I'm sure there's a better way to do this, I just don't know how
        Check to see if the fourth or fifth argument is an object or not, create new PDF object to be added to options object 
      */
      if(isObject(arguments[3])){
        opt.pdf = {sheet: arguments[3].sheet, name: arguments[3].name, type: arguments[3].type}
        tempName(opt)
      } else if(isObject(arguments[4])){
        opt.pdf = {sheet: arguments[4].sheet, name: arguments[4].name, type: arguments[4].type}
        tempName(opt)
      }
    }
    
    function tempName(opt){
      opt.pdf.name == null ? opt.pdf.name = opt.pdf.sheet : null
      opt.pdf.type == null ? opt.pdf.type = 'portrait' : null
      return opt.attachments = [pdf(opt.pdf.sheet, opt.pdf.type).setName(opt.pdf.name)]
    }
    MailApp.sendEmail(opt)
  } catch(e){
    Logger.log(e)
  }
}

Other Relevant Code

This function takes in both the sheet name and type, and will convert it into a PDF based on those values

  function pdf(sheetName, type){
    let ss_id = spreadsheet.getId()
    let sheet_id = spreadsheet.getSheetByName(sheetName).getSheetId()
    let url
    //Spreadsheet converted to PDF in portrait mode
    type == 'portrait' ? url = "https://docs.google.com/spreadsheets/d/" + ss_id + "/export?format=pdf&gid=" + sheet_id : null
    //Spreadsheet converted to PDF in landscape mode
    type == 'landscape' ? url = "https://docs.google.com/spreadsheets/d/" + ss_id + "/export?format=pdf&portrait=false&gid=" + sheet_id : null
    let response = UrlFetchApp.fetch(url, {
      muteHttpExceptions: true,
      headers: {
        Authorization: 'Bearer ' + ScriptApp.getOAuthToken(),
      },
    }).getBlob()
    return response  
  }

At my company, I am not able to use APIs for various reasons, so I had to find a work around to validate the emails. I go into more depth about this function in my answer here. To note: this function does make the program run more slowly, and it does not currently work with arrays, only with individual emails.

  //Checker to ensure email exists
  function validateEmail(email){
    let ss = SpreadsheetApp.openByUrl(SpreadsheetApp.create('Email Validation Spreadsheet', 1, 1).getUrl())
    if(!new RegExp('[@]').test(email)){
      return false
    } else{
      try{
        ss.addViewer(email)
      } catch(e){
        setTrashed()
        return false
      }
      setTrashed()
      return true
    }
    function setTrashed(){
      DriveApp.getFilesByName('Email Validation Spreadsheet').next().setTrashed(true)
    }
  }

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.