Add the User’s Signature Block to an Automated Gmail Email with Apps Script

Sending out emails as a part of a Google Workspace automated workflow is a very common task. In Google Apps Script we can send emails to users using the MailApp.sendEmail(), the GmailApp.sendEmail() method or even as a JSON payload with the Gmail Advanced API service.

While one might expect that the sender’s signature block would also be transmitted with the automated email, we find that this is not in fact the case.

So what do we do?

The video:

Extract the Primary Signature Block with Apps Script

Let’s say we want to add the following signature block to an automated email.

Gmail Signature Block example for Apps Script

To do this, we will need to use the Gmail API advanced service.

  1. In your Apps Script project, select the Add a service plus button
  2. A dialogue box will appear. Scroll down until you find Gmail.
  3. Keep the identifier as it is and then select Add. 
  4. The service should be added to the sidebar.

Gmail Advanced Service added to Apps Script IDE

From here we can create a one-liner to extract the primary signature block HTML from the sender’s Gmail account.

The logged results for our example are a HTML string that looks like this:

I don’t see my signature HTML

If, after logging your signature and you don’t see anything, check your email configuration in Gmail under Settings > General > Signature. Ensure that you have selected a signature as default, ‘For new email use’.

Gmail Signature set as default

Understanding the One-Liner

Gmail.Users.Settings.SendAs ...

This first section of the code identifies the API path. Here we are looking at the sender’s (user’s) settings for any of the user’s aliases.

... .list("me") ...

To extract the details of the settings for the active user, the person running the script, we can use the shorthand, “me”, rather than identifying a specific email.

From this point, we would get an array of objects, one for each of the user’s aliases.

For our example user, the object would look like this:

... .sendAs. ...

First, to get into the array we need to call the sendAs property.

We wouldn’t want to extract a signature block another alias so we will ensure that we are using our default email.

... .find(account => account.isDefault) ...

To do this we can use the JavaScript ‘find’ method. This takes a function as an argument. Here we have used a simple arrow function where ‘account’ is our iterator for each array. If the default for the currently iterated account is true then we want to extract the signature string.

... .signature

Finally, we extract the signature as a HTML string.

Adding it to your code

Here is a simple example of how you can append the signature to an email.

It is important to note here that instead of adding a plain text body to the sendEmail method of GmailApp (Line 13) we have instead used the optional object insertion and added the htmlBody property.

Then, above on line 10, we combine the message with the signature ensuring we have a couple of line breaks between the two to provide good separation.

Note that you may even need to add further HTML formatting to the original message to ensure line spacing is maintained.

Accessing your other Gmail Signatures with Apps Script

So, you might be thinking,

‘I like to send different signature blocks for different circumstances. How do I do that?’

Whelp, my dear friends, as of writing this, you can’t access them at all using Gmail Advanced services. Indeed many developers are more than a little miffed at this and have been so for a number of years.

‘Well now! I’m a bit cranky about this too, if only there were a place to proactively express my displeasure at those who may be able resolve this problem.’

Support the current feature request

Protest access to all signature blocks in Gmail with Gmail API

Head over to Google’s issue tracker now and add your vote and perhaps, a comment to request this feature:

Google IssueTracker

Plan B: Using Drafts

In the meantime, we can store our alternate signature in a draft email with a unique subject line.

Here is our example of the secondary signature as a draft email:

Gmail signature block stored in a draft email to be extracted for automation

We can call this function from wherever we are sending our email. The draftTextSignature function takes the subject line of our draft email containing our desired signature block. So for our example, it would be this:

const signature = draftTextSignature("Gmail Signature Block 2");

Line 8: Retrieves all the user’s draft emails with the getDrafts() method of the GmailApp class.

Then we again use the JavaScript ‘find’ method to search for the draft containing our desired subject line.  Keep in mind that ‘find’ takes a function with an iterating parameter we set to ‘signature’.

Line 10: As the code looks through each draft we extract the subject line from the draft message.

Line 12: We then compare the current subject with our target subject line and if there is a match, ‘find’ stops and returns the current draft.

Line 16: Finally, we retrieve the body from the found message.

All-in-one Retrieve Selected Gmail Signature Function for Apps Script

If you want to have the flexibility to either extract the primary signature or retrieve one from a predefined Gmail draft then you could use this code:

Try this runsies() function out to test the code:

Just make sure you change the getSignatureBlock argument to the subject line for the draft email containing your signature.

 

~Yagi

Google Apps Script – How to Automatically Generate a Time From a Four Digit 24 hour Time in Google Sheets.

Google Apps Script, Google Sheets

On a recent board post, a Google Sheets user wanted to change a four-digit number (for example, 1230) to a time, like 12:30, in the same cell that the item was entered.

Unfortunately, the user was not in a position to change the starting values, so they were left with the 4 digits.

There are two ways of doing this with varying levels of complexity:

 

  1. The Google Sheets Formula Approach
  2. The Google Apps Script onEdit Approach

Continue reading “Google Apps Script – How to Automatically Generate a Time From a Four Digit 24 hour Time in Google Sheets.”

Find and Hide Rows in Google Sheets with Apps Script

Using the Spreadsheet App Class’ Text Finder Class to find and hide rows in Google Sheets containing target text in a cell can be a fast way to hide values. In many situations, this may be a faster way to hide rows based on cell value in Google Sheets with Google Apps Script than iterating through a range and extracting rows.

In this tutorial, we will cover 3 approaches to using the Text Finder class to hide rows. Each may be useful in its own circumstances.

This tutorial accompanies the YouTube video series of the same name. You can find the links to each of the related videos in each of the sections along with the starter Google Sheets so that you can play along.

V1 – Basic Find and Hide Rows based on cell values in Google Sheets

Starter Sheet

Version 1 – Starter Sheet

The Video

https://youtu.be/alI2f7w7xjU

The Code

Unlike when formatting rows and cell activation, – as we did in the previous tutorial – our fasted approach here is to hide and unhide sheets while looping through all the found cells.

In this basic approach, our function contains 3 parameters:

  • text – The text to search.
  • sheetName – The sheet name to search.
  • isHide – An optional argument set to true by default to hide values or false manually to unhide them.

Lines 10-11 – We first collected our current Google Sheet workbook and then select the sheet tab we will be working in.

Lines 13-14 – Then we use the Text Finder class in the Spreadsheets App to search for our target text and then use the findAll() method to get an array constructor of all the found value cell ranges. You can learn more about this in the first tutorial in this series here:

Find All Values in Google Sheets with Apps Script

Line 16 – Next we iterate through each found row with a JavaScript forEach() loop.

Line 17 – On each iteration, we collect the row number. We do the same thing here in our previous tutorial when activating and formatting entire rows.

Lines 19-23 –  Lastly we need to check if the user has set isHide to false to show the rows or true (or not used) to hide the row. We then call the sheet and apply either the Spreadsheets Apps Hide Rows (hideRows()) method or (showRows()) Show Rows method. These methods can take a single row as an argument. We provide this with the row variable.

This is a quick and easy solution to understand. However, it does not perform well with a large dataset. It will also try and hide rows multiple times when a found cell is on the same row as a previously found cell.

The function, just like the other two examples, can be called from another function with:

hideAllRowsWithVal(text, sheetName, boolean)

e.g.:

hideAllRowsWithVal("koala", "Sheet1", true)

If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.

V2 – Find and Hide Rows based on cell values in Google Sheets with Range Grouping to improve performance

Starter Sheet

Version 2 – Starter Sheet

The Video

https://youtu.be/CeWUAOK7Ui8

Released Monday 12 Feb 2023. Subscribe (Top right) to get a notification for when this video comes out. 

The Code

In larger, ranges we might have a lot of situations where our found text is on multiple adjacent rows.

Find and hide rows by grouping ranges in Google Sheets with Apps Script

In the image above, we can see that rows 30 and 31, 38-43, and 46-49 can all be batched together into a single range.

Furthermore, rows with the found text (Koala) on the same row can be ignored, the consecutive times that they are found.

Let’s go ahead and fix our code to make it more efficient.

Note that lines 9-16 of the code are the same as the previous function. Refer to this for an explanation.

The Hide Range METHOD

Line 22 – After we have collected our ranges of found rows we will create a hideRange() method. This will either hide or show the range based on the isHide argument.

This function takes a single object parameter that contains a start row and a number of rows.

Lines 24 – 26 – Unlike the previous version, we use the Hide Rows and Show Rows extra parameter to include the number of rows deep to hide.

Row Range variable

Lines 32 – 35 – This mutable variable stores the start row and row depth as we collected each range of rows to hide in our sheet.

Iterate through all cells

Line 37 – Here, we start the forEach loop that iterates through each found cell range. We include the index (idx) in our arguments in our arrow function as well here.

Line 39 – Next, we grab the row number with the Get Row (getRow()) method.

Line 40 – Then we get the next possible row in the current range collection by adding the start row with the row depth. This will be used in a moment ot compare against the currently iterated row.

On the first iteration

Lines 42-46 – On our first loop through our found cells, all we need to do is add our first found row and add one to our depth.

There is another cell on the same row

Lines 47-51 – If there is another cell on the same row, then we don’t need to do anything and we simply return the function for that iteration.

There is a cell directly below the previous one

Lines 52-55 – If the next found row is only one cell down (adjacent), then we just want to add one to the number of rows of the existing rowRange variable.

Lines 57-60 – If the row is the last found row, then we can run the hideRange function on our update range set.

A new range begins

If the next found row is not directly below the previous one. We need to:

  1. Line 63 – Run the hideRange on the current rowRange variable set.
  2. Lines 65-66 – Reset the rowRange variable adding in the current row to the start range and one to the depth.

Lines 69-71 – Again, if we are on the last found row, we can just run hideRow to end the loop.

Create and Publish a Google Workspace Add-on with Apps Script Course

Need help with Google Workspace development?

My team of experts can help you with all of your needs, from custom app development to integrations and security. We have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.

Schedule a free consultation today to discuss your needs and get started or learn more about our services here.

V3 – Find and Hide Rows based on cell values in Google Sheets using the Google Sheets Advanced Service API and Range Grouping

The Video

Released Thursday 13 Feb 2023. Subscribe (Top right) to get a notification when this video comes out. 

Starter Sheet

Version 3 – Starter Sheet

The Code

I found version two to be remarkably efficient. However, if you want to do a lot of editing to the sheet tab then you might want to consider using the Google Apps Script Advanced Sheet Service API.

Main Variables

We need to add a few more variables to our main variables here.

Line 12 – We will need to get the spreadsheet ID to use in our batch update.

Line 13 – The sheet name does not work the same as an identifier in the advanced service, so we will need to extract the sheet ID too.

A request list

Line 21 – The Sheets batch request property requires a list of requests as its value. Here we will store an array of all the requests that we want to batch together to hide each row range of the Google Sheet.

Update Dimension Properties

Line 27 – The appendRequest(setRange) function pushes a new request to the requests property.

When hiding and displaying rows or columns in a Google Sheet with the Advanced Service we are updating a dimension property. These properties can be a number of different field types, but for us, we want to hide rows.

Let’s  take another look at the layout of this JSON object:

Lines 3-8 – The first sub-property is the range object. This object requires a sheet ID. The dimension is either the columns or rows.

Then we need to set the start index. Note that the cell ranges will start from zero instead of 1. We must then subtract 1 from our start row to get the correct start index of our row range.

Finally, we set the end row. This will be one row after our desired end row. Think, ‘up to, but not including this row’.

Lines 9-11 – Next, we identify the property that we want to change. For hiding and showing rows this is the "hiddenByUser" property, where the value is a boolean. This conveniently fits with our ishide parameter.

Line 12 – Weirdly, we then need to declare that we are using the "hiddenByUser" property by adding it to the fields list.

Store the current row range

Lines 46-49 – Just like the previous version, we need to store each range before sending it to appendRequest(). Unlike the previous version, our last property is the end row rather than the number of rows.

Starting the loop

Lines  52-54 –Now we can commence our loop through the found cells.

Here, the first task is to collect all the row numbers.

On the first iteration

Lines 56-60 – On the first loop of our cell array, we just want to add the current row number to the start and end row of rowRange.

If the next row is on the same row

Lines 61 – 65 – If we have more than one result on the same row we want to ignore it and return the current loop.

If its the next row below

Line 69 – We add one to our end row value in rowRange.

Lines 72-74 – If we are on our last found row, send it to appendRequest().

Create a new row range

Line 77 – If the current row is not the next row down then we send appendRequest() with our current rowRange.

Lines 79-80 – Next, we create a new rowRange.

Line 83-85 – If the current row is the last row in the array, we sent it to appendRequest().

The Batch request

Line 90. Here we use the batch request of the spreadsheet resource. The batch request takes an object with a requests property. This property, in turn, requires an array of JSON object requests.

For its second argument, we reference the Spreadsheet ID.

 

Some notes

If you know that there may not be any matches in your text finder then you might wish to add a return agent after the allOccurrences variable:

if(allOccurences.length === 0) return;

Another good measure might be to use a try/catch statement in the third version when running the batch call, just in case there is an error with the API server.

If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.

~ Yagi

 

 

Performance of Google Apps Script Text Finder Class on 2 Approaches to Searching Large Datasets

Inspired by research into a recent blog post, the Google Apps Script Text Finder Class’ Find All (findAll()) and Find Next (findNext()) methods were benchmarked over two different datasets containing 50,000 rows. The first dataset contained 1,000 cells matching the search text. The second dataset contained 100 matching cells.

For each dataset, a test was conducted to retrieve either the first 10 matching cells or the first 100 matching cells. The Find All and Find Next approaches were tested and compared on each test.

It was expected that Find Next would perform best on the condition where the dataset contained a large number of found items and only a small number of first cells needed to be reported. The benchmark results suggest that this hypothesis is most likely.

First number of cells to retrieve Test Function Avg. run time over 100 runs. Fastest Function Fastest Avg. Time Avg. time Difference
1000 items to find
10
1
v2 findAll 1626.24 v3 findNext 1368.45 257.79
v3 findNext 1368.45
50
2
v2 findAll 1578.19 v2 findAll 1578.19 4993.61
v3 findNext 6571.8
100 items to find
10
3
v2 findAll 360.94 v2 findAll 360.94 975.16
v3 findNext 1336.1
50
4
v2 findAll 377.13 v2 findAll 377.13 6175.59
v3 findNext 6552.72

Table: The average time in milliseconds of 100 runs of each test of Apps Script Text Finder findAll() and findNext() methods. Image link for mobile-friendly viewers.

Method

Sample Data

Two columns of data 50,000 rows deep were generated for this test. Each cell in each column consisted of a number; either 1, 2, 3, 4 or 5. An equal spread of numbers 1 through 4 where added to each row. Each column differs by the number of 5s in each row:

  • Col A: 1,000 5’s
  • Col B: 100 5’s

Each column was then selected and randomised with: Data > Randomise range.

Test

Two functions are compared to test their performance based on four test conditions based on 100 runs of each test:

  1. Retrieve the first 10 cells containing the search text where the range contains 1,000 matching search items.
  2. Retrieve the first 50 cells containing the search text where the range contains 1,000 matching search items.
  3. Retrieve the first 10 cells containing the search text where the range contains 100 matching search items.
  4. Retrieve the first 50 cells containing the search text where the range contains 100 matching search items.

The time in milliseconds was recorded using the JavaScript Date.now() method before and after the functions were run. The difference in time in milliseconds was then appended to an array and added to a Google Sheet column for each test type. This culminated in 8 sets of 100 results.

The average of each test was then recorded and used to compare performance.

Note: Performance.now() is not available in Google Apps Script. 

Code

All code and results can be found copied from this sheet:

Analysis of Google Apps Script Create Finder Class Retrieve n found values

To explore the code and run your own independent tests, make a copy of the Google Sheet: File > Make a copy.

More detailed breakdowns of the code for each test function can be found in the source tutorial.

Note! There is no v1. The version numbers refer to the tutorial related to this post.

Main Test RUN

This function ran all the test conditions. Modify colPastePosition to add the culminated times to the desired columns. Then uncomment the desired run.

test_v2 – Google Apps Script Text Finder Class- findAll()

Code breakdown can be found here: link.

This function retrieves the full list of all found cells using the findAll() method from the Text Finder Class. All available found items in the range are then stored in the found variable.

It then relies on a for-loop to iterate through each cell and collect the cell location using the Spreadsheet App Class’ range getA1Notation method. Each cell location is then stored in the locations variable as an array item before returning the array to the initialising function.

The for-loop breaks when the total number of required cell items (the position) equal the index variable (i) in the loop.

test_v3 – Google Apps Script Text Finder Class- findNext()

Code breakdown can be found here: link.

In this function, a call is made to the spreadsheet to retrieve the found cell value each time findNext() method of the Text Finder Class is called. On each iteration, the getA1Notation method is used to retrieve the cell location. This location is then stored as an array value in the locations variable before being returned to the initiating function.

The function used a while-loop to iterate through each next item found until the counter – or the number of required cells to collect – is reached.

Results & Discussion

Analysis of Google Apps Script Create Finder Class Retrieve n found values
Performance in Milliseconds to Retrieve the first 10 or 50 Matching Values over a 50,000 Row Range Contain Either 1000 or 100 Matchable items Using the Google Apps Script Spreadsheet App Finder Class.

Test 1: Retrieve the first 10 cells containing the search text where the range contains 1,000 matching search items.

Version 3 –findNext() performed better on average when there were 1000 potential items to find in the range but only the first 10 items need to be selected. Versions 3’s average speed was 1368.45ms compared to version 2’s average run speed of 1826.24ms. This is a performance increase of 257.79ms for version 3.

Version 2’s lower performance is likely due to needing to collect all available found cells before it can extract the top 10 items.

Version 3, makes 10 calls to the Google Sheets in this example. Compared to version 2, this takes relatively less time than collecting all available found cell references to the search item.

TEST 1: 1,000 randomised items to find in 50,000. Return first 10 matches using v2-findAll and v3-findNext functions.
TEST 1: 1,000 randomised items to find in 50,000. Return first 10 matches using v2-findAll and v3-findNext functions.

Test 2: Retrieve the first 50 cells containing the search text where the range contains 1,000 matching search items.

Version 2 – findAll() performed significantly better over 100 runs than version 3 when retrieving the top 50 found cells from a possible 1000. Version 2 was, on average, 4993.61ms faster at an average runtime of 1578.19ms compared to version 3’s sluggish 6571.80ms average.

It was expected that test one and test two’s times for version 2 would be similar and there are only 48.05ms between their average runtimes.

Version 3’s poor performance is likely due to its reliance on calling the spreadsheet to collect the cell data on all 50 calls it needs to make.

TEST 2: 1,000 randomised items to find in 50,000. Return first 50 matches using v2-findAll and v3-findNext functions.
TEST 2: 1,000 randomised items to find in 50,000. Return first 50 matches using v2-findAll and v3-findNext functions.

Test 3: Retrieve the first 10 cells containing the search text where the range contains 100 matching search items.

Version 2, again, performed better by 975.16ms than version 3 when there was a smaller potential number of items to find in the range and only the first ten items need to be retrieved.

Here the performance margin between the two versions was closer than in the previous test. Version 2’s average run speed was 360.94ms while version 3’s runtime was 1336.10ms.

With a smaller number of retrieved items, the version 2 findAll() function did not have to work as hard to collect the methods related to each range it collects. Whereas version 3 still needed to make 10 performance-intensive calls back to the Google Sheet each time with relatively no performance change to test one.

TEST 3: 100 randomised items to find in 50,000. Return first 10 matches using v2-findAll and v3-findNext functions.
TEST 3: 100 randomised items to find in 50,000. Return first 10 matches using v2-findAll and v3-findNext functions.

Test 4: Retrieve the first 50 cells containing the search text where the range contains 100 matching search items.

Predictably, version 2 – findAll() performed the best when the expected match sample is small (100 available matches) and the total first set of cells to retrieve was relatively large (50).

Version 2’s average completion time was 377.13ms compared to version 3’s average of 6552.72ms, performing on average 6175.59ms faster. This is by far the largest margin on performance between the two versions.

Here again, version 3 must perform 50 calls to the Google Sheet, each one retrieving the cell range data. Alternatively, version 2 makes one call to the spreadsheet and then retrieves the cell data for all collected values. This is significantly faster than version 3’s approach.

TEST 4: 100 randomised items to find in 50,000. Return first 50 matches using v2-findAll and v3-findNext functions.
TEST 4: 100 randomised items to find in 50,000. Return first 50 matches using v2-findAll and v3-findNext functions.

Overall

On datasets that may have the potential to contain a large number of matching items, but fewer required results to return, version 3 may be the best option. In all other cases, version 2 is the most optimal approach to finding data in a range.

It is important to note that it can be difficult to accurately measure performance with Apps Script runs because resource allocation to run a script does seem to vary. Nevertheless, with a sample size of 100 runs, it is hoped that average values will be more accurate than a smaller sample.

Grab Your Own Copy of the Google Sheet and Attached Code here

If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.

Create and Publish a Google Workspace Add-on with Apps Script Course

Need help with Google Workspace development?

My team of experts can help you with all of your needs, from custom app development to integrations and security. We have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.

Schedule a free consultation today to discuss your needs and get started or learn more about our services here.

~Yagi

 

Find All Values in Google Sheets with Apps Script

Just like when using the Ctrl + F shortcut in Google Sheets to find values in your spreadsheet, there is a class in Google Apps Script that can do the same thing.

This could be a useful tool as a part of an automation process. For example, finding the location of a value and applying formatting to it or copying the cell’s entire data into a separate location if the value is a part of a larger text in the cell.

This tutorial accompanies the YouTube video tutorial of the same name.

Grab a copy of the starter sheet to play along and get the most out of the video.

Starter Sheet

Find All Apps Script – Starter Sheet

If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.

The Code

Note that for the video tutorials, I have added the variables (e.g. the Sheet Name, Range) inside each function. It is usually good practice to keep these functions independent and call them from other functions. This makes them more reusable.

Instead, we can add the variables we need as parameters for the functions and return the result. See the example below:

These examples use the TextFinder class as a part of the Google Apps Script Sheets App Class.

In these examples, we use the findAll method of this class. This will return an array containing all the cells containing the selected value searched. From here, you can treat each cell as a range and call range methods like:

  • Get A1 Notation.
  • Get Sheet – Get Name.
  • Get Row.

We use the JavaScript Map method to iterate through each item that we find.

Find All Values in All Sheets

This finds all values in all sheets and returns an array containing an object for each sheet containing the sheet name and the cell location.

Returns:

 

Find All Values in Selected Sheets

Finds all the values in a selected sheet and returns an array identifying the cell that each item is found.

Returns:

[ 'A3', 'A4', 'F14', 'A16' ]

Find All Values in a Selected Range

Finds the values for any item in a selected range and returns the row the item was found on.

Example 1

Returns:

[ 3, 4, 16 ]

Example  2

Returns:

[ 1, 2, 4, 5 ]

The Video

Create and Publish a Google Workspace Add-on with Apps Script Course

Need help with Google Workspace development?

My team of experts can help you with all of your needs, from custom app development to integrations and security. We have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.

Schedule a free consultation today to discuss your needs and get started or learn more about our services here.