GmailApp.sendEmail: is it possible to schedule send an email? - google-apps-script

I try to create my mail merge with this example.
However, I don't want to send all emails immediately after using the script - I'd like to create scheduled messages with dates specified in a column (Scheduled time).
The script from the example:
// To learn how to use this script, refer to the documentation:
// https://developers.google.com/apps-script/samples/automations/mail-merge
/*
Copyright 2022 Martin Hawksey
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* #OnlyCurrentDoc
*/
/**
* Change these to match the column names you are using for email
* recipient addresses and email sent column.
*/
const RECIPIENT_COL = "Recipient";
const EMAIL_SENT_COL = "Email Sent";
/**
* Creates the menu item "Mail Merge" for user to run scripts on drop-down.
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Mail Merge')
.addItem('Send Emails', 'sendEmails')
.addToUi();
}
/**
* Sends emails from sheet data.
* #param {string} subjectLine (optional) for the email draft message
* #param {Sheet} sheet to read data from
*/
function sendEmails(subjectLine, sheet=SpreadsheetApp.getActiveSheet()) {
// option to skip browser prompt if you want to use this code in other projects
if (!subjectLine){
subjectLine = Browser.inputBox("Mail Merge",
"Type or copy/paste the subject line of the Gmail " +
"draft message you would like to mail merge with:",
Browser.Buttons.OK_CANCEL);
if (subjectLine === "cancel" || subjectLine == ""){
// If no subject line, finishes up
return;
}
}
// Gets the draft Gmail message to use as a template
const emailTemplate = getGmailTemplateFromDrafts_(subjectLine);
// Gets the data from the passed sheet
const dataRange = sheet.getDataRange();
// Fetches displayed values for each row in the Range HT Andrew Roberts
// https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490
// #see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues
const data = dataRange.getDisplayValues();
// Assumes row 1 contains our column headings
const heads = data.shift();
// Gets the index of the column named 'Email Status' (Assumes header names are unique)
// #see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions
const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL);
// Converts 2d array into an object array
// See https://stackoverflow.com/a/22917499/1027723
// For a pretty version, see https://mashe.hawksey.info/?p=17869/#comment-184945
const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {})));
// Creates an array to record sent emails
const out = [];
// Loops through all the rows of data
obj.forEach(function(row, rowIdx){
// Only sends emails if email_sent cell is blank and not hidden by a filter
if (row[EMAIL_SENT_COL] == ''){
try {
const msgObj = fillInTemplateFromObject_(emailTemplate.message, row);
// See https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)
// If you need to send emails with unicode/emoji characters change GmailApp for MailApp
// Uncomment advanced parameters as needed (see docs for limitations)
GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {
htmlBody: msgObj.html,
// bcc: 'a.bbc#email.com',
// cc: 'a.cc#email.com',
from: 'sender#gmail.com',
name: 'name of the sender',
// replyTo: 'a.reply#email.com',
// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)
attachments: emailTemplate.attachments,
inlineImages: emailTemplate.inlineImages
});
// Edits cell to record email sent date
out.push([new Date()]);
} catch(e) {
// modify cell to record error
out.push([e.message]);
}
} else {
out.push([row[EMAIL_SENT_COL]]);
}
});
// Updates the sheet with new data
sheet.getRange(2, emailSentColIdx+1, out.length).setValues(out);
/**
* Get a Gmail draft message by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} containing the subject, plain and html message body and attachments
*/
function getGmailTemplateFromDrafts_(subject_line){
try {
// get drafts
const drafts = GmailApp.getDrafts();
// filter the drafts that match subject line
const draft = drafts.filter(subjectFilter_(subject_line))[0];
// get the message object
const msg = draft.getMessage();
// Handles inline images and attachments so they can be included in the merge
// Based on https://stackoverflow.com/a/65813881/1027723
// Gets all attachments and inline image attachments
const allInlineImages = draft.getMessage().getAttachments({includeInlineImages: true,includeAttachments:false});
const attachments = draft.getMessage().getAttachments({includeInlineImages: false});
const htmlBody = msg.getBody();
// Creates an inline image object with the image name as key
// (can't rely on image index as array based on insert order)
const img_obj = allInlineImages.reduce((obj, i) => (obj[i.getName()] = i, obj) ,{});
//Regexp searches for all img string positions with cid
const imgexp = RegExp('<img.*?src="cid:(.*?)".*?alt="(.*?)"[^\>]+>', 'g');
const matches = [...htmlBody.matchAll(imgexp)];
//Initiates the allInlineImages object
const inlineImagesObj = {};
// built an inlineImagesObj from inline image matches
matches.forEach(match => inlineImagesObj[match[1]] = img_obj[match[2]]);
return {message: {subject: subject_line, text: msg.getPlainBody(), html:htmlBody},
attachments: attachments, inlineImages: inlineImagesObj };
} catch(e) {
throw new Error("Oops - can't find Gmail draft");
}
/**
* Filter draft objects with the matching subject linemessage by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} GmailDraft object
*/
function subjectFilter_(subject_line){
return function(element) {
if (element.getMessage().getSubject() === subject_line) {
return element;
}
}
}
}
/**
* Fill template string with data object
* #see https://stackoverflow.com/a/378000/1027723
* #param {string} template string containing {{}} markers which are replaced with data
* #param {object} data object used to replace {{}} markers
* #return {object} message replaced with data
*/
function fillInTemplateFromObject_(template, data) {
// We have two templates one for plain text and the html body
// Stringifing the object means we can do a global replace
let template_string = JSON.stringify(template);
// Token replacement
template_string = template_string.replace(/{{[^{}]+}}/g, key => {
return escapeData_(data[key.replace(/[{}]+/g, "")] || "");
});
return JSON.parse(template_string);
}
/**
* Escape cell data to make JSON safe
* #see https://stackoverflow.com/a/9204218/1027723
* #param {string} str to escape JSON special characters from
* #return {string} escaped string
*/
function escapeData_(str) {
return str
.replace(/[\\]/g, '\\\\')
.replace(/[\"]/g, '\\\"')
.replace(/[\/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t');
};
}
Is it possible to set sending date with the GmaillApp.sendEmail function? I wasn't able to find a way in the documentation.
If not, is there another way to achieve this with Google Sheets? Any suggestion will be appreciated.

Scheduling with Google Apps Script is possible using Triggers.
Click Triggers on left menu.
Then "Add Trigger" button and select the function and time to run automatically.
To run your function with an specific date from your cell, see this other question: Run trigger at specific date and time
Documentation: https://developers.google.com/apps-script/reference/script/clock-trigger-builder
Hope it helps.

Yes, almost anything is possible with AppScript.
I suggest you read the sheet into an array and then create a loop that compares the current time to the scheduled time. If they are equal (may not be able to get it to the exact second, minutes likely would be better), sends the email and removes the element from the array. The loop continues until the array is empty.

Related

Curly brackets to %7 in Gmail

I followed this tutorial used to fill up a html template using Gmail. So in my google sheet document I provide a column with data and in the html template I use the {{Name}} and this script replace the data and sends the mail:
// Copyright Martin Hawksey 2020
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
/**
* #OnlyCurrentDoc
*/
/**
* Change these to match the column names you are using for email
* recipient addresses and email sent column.
*/
const RECIPIENT_COL = "Recipient";
const EMAIL_SENT_COL = "Email Sent";
/**
* Creates the menu item "Mail Merge" for user to run scripts on drop-down.
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Mail Merge')
.addItem('Send Emails', 'sendEmails')
.addToUi();
}
/**
* Send emails from sheet data.
* #param {string} subjectLine (optional) for the email draft message
* #param {Sheet} sheet to read data from
*/
function sendEmails(subjectLine, sheet=SpreadsheetApp.getActiveSheet()) {
subjectLine = "Beste {{Voornaam}}, vul nu jouw persoonlijke progressie & feedback maaltijdplan in!";
// get the draft Gmail message to use as a template
const emailTemplate = getGmailTemplateFromDrafts_(subjectLine);
// get the data from the passed sheet
const dataRange = sheet.getDataRange();
// Fetch displayed values for each row in the Range HT Andrew Roberts
// https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490
// #see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues
const data = dataRange.getDisplayValues();
// assuming row 1 contains our column headings
const heads = data.shift();
// get the index of column named 'Email Status' (Assume header names are unique)
// #see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions
const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL);
// convert 2d array into object array
// #see https://stackoverflow.com/a/22917499/1027723
// for pretty version see https://mashe.hawksey.info/?p=17869/#comment-184945
const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {})));
// used to record sent emails
const out = [];
// loop through all the rows of data
obj.forEach(function(row, rowIdx){
// only send emails is email_sent cell is blank and not hidden by filter
if (row[EMAIL_SENT_COL] == ''){
try {
const msgObj = fillInTemplateFromObject_(emailTemplate.message, row);
// #see https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)
// if you need to send emails with unicode/emoji characters change GmailApp for MailApp
// Uncomment advanced parameters as needed (see docs for limitations)
GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {
htmlBody: msgObj.html,
// bcc: 'a.bbc#email.com',
// cc: 'a.cc#email.com',
from: 'Info#example.com',
name: 'Example',
// replyTo: 'a.reply#email.com',
// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)
attachments: emailTemplate.attachments,
inlineImages: emailTemplate.inlineImages
});
// modify cell to record email sent date
//out.push([new Date()]);
} catch(e) {
// modify cell to record error
//out.push([e.message]);
}
} else {
out.push([row[EMAIL_SENT_COL]]);
}
});
// updating the sheet with new data
//sheet.getRange(2, emailSentColIdx+1, out.length).setValues(out);
/**
* Get a Gmail draft message by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} containing the subject, plain and html message body and attachments
*/
function getGmailTemplateFromDrafts_(subject_line){
try {
// get drafts
const drafts = GmailApp.getDrafts();
// filter the drafts that match subject line
const draft = drafts.filter(subjectFilter_(subject_line))[0];
// get the message object
const msg = draft.getMessage();
// Handling inline images and attachments so they can be included in the merge
// Based on https://stackoverflow.com/a/65813881/1027723
// Get all attachments and inline image attachments
const allInlineImages = draft.getMessage().getAttachments({includeInlineImages: true,includeAttachments:false});
const attachments = draft.getMessage().getAttachments({includeInlineImages: false});
const htmlBody = msg.getBody();
// Create an inline image object with the image name as key
// (can't rely on image index as array based on insert order)
const img_obj = allInlineImages.reduce((obj, i) => (obj[i.getName()] = i, obj) ,{});
//Regexp to search for all img string positions with cid
const imgexp = RegExp('<img.*?src="cid:(.*?)".*?alt="(.*?)"[^\>]+>', 'g');
const matches = [...htmlBody.matchAll(imgexp)];
//Initiate the allInlineImages object
const inlineImagesObj = {};
// built an inlineImagesObj from inline image matches
matches.forEach(match => inlineImagesObj[match[1]] = img_obj[match[2]]);
return {message: {subject: subject_line, text: msg.getPlainBody(), html:htmlBody},
attachments: attachments, inlineImages: inlineImagesObj };
} catch(e) {
throw new Error("Oops - can't find Gmail draft");
}
/**
* Filter draft objects with the matching subject linemessage by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} GmailDraft object
*/
function subjectFilter_(subject_line){
return function(element) {
if (element.getMessage().getSubject() === subject_line) {
return element;
}
}
}
}
/**
* Fill template string with data object
* #see https://stackoverflow.com/a/378000/1027723
* #param {string} template string containing {{}} markers which are replaced with data
* #param {object} data object used to replace {{}} markers
* #return {object} message replaced with data
*/
function fillInTemplateFromObject_(template, data) {
// we have two templates one for plain text and the html body
// stringifing the object means we can do a global replace
let template_string = JSON.stringify(template);
// token replacement
template_string = template_string.replace(/{{[^{}]+}}/g, key => {
return escapeData_(data[key.replace(/[{}]+/g, "")] || "");
});
return JSON.parse(template_string);
}
/**
* Escape cell data to make JSON safe
* #see https://stackoverflow.com/a/9204218/1027723
* #param {string} str to escape JSON special characters from
* #return {string} escaped string
*/
function escapeData_(str) {
return str
.replace(/[\\]/g, '\\\\')
.replace(/[\"]/g, '\\\"')
.replace(/[\/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t');
};
}
Now I want add link. The first time it works well but after a while Gmail makes from this http://{{Link}} to this http://%7B%7BLink%7D%7D.
Gmail screenshot 1
Gmail screenshot 2
I use Stripo to make my template and this is the code:
<td class="esd-block-button es-p40t es-p40b es-p25r es-p25l" align="center" bgcolor="#ffffff"><span class="es-button-border" style="border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; border-color: #213f92; background-color: #234290; border-width: 1px;">Progressie formulier</span></td>
So I included the {{Link}} in the A href.
My questions:
Would changing the {{}} to something like this %%%% or this <<>> gmail not changing my merge tag?
Any other ideas how I can fix this would be awesome!
Sorry for the bad English btw :)
Buddy of my helped me with the solution
function fillInTemplateFromObject_(template, data) {
// Google mail when opening drafts with URL's in it do not understand {{}}, however we need {{}} for the replace logic.
template.text = template.text.replace("((Link))", "{{Link}}");
template.html = template.html.replace("((Link))", "{{Link}}");
// we have two templates one for plain text and the html body
// stringifing the object means we can do a global replace
let template_string = JSON.stringify(template);
// token replacement
template_string = template_string.replace(/{{[^{}]+}}/g, key => {
return escapeData_(data[key.replace(/[{}]+/g, "")] || "");
});
return JSON.parse(template_string);
}
Add in the Gmail url draft (()) instead of {{}} and it works with above changed code

How to use variable for email cc field in Google Apps Script?

I'm using a very slightly adapted copy of this apps script to do a mail merge in Gmail using a Google Sheet that has various data. I have a column in the sheet for CC recipients who I also want the emails to go to. But I'm not sure how to use this in the Apps script.
The code that I need to adjust is below, but how do I add the cc emails from the CC column in the Sheet?
// Copyright Martin Hawksey 2020
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
/**
* #OnlyCurrentDoc
*/
/**
* Change these to match the column names you are using for email
* recipient addresses and email sent column.
*/
const RECIPIENT_COL = "Recipient";
const EMAIL_SENT_COL = "Email Sent";
/**
* Creates the menu item "Mail Merge" for user to run scripts on drop-down.
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Mail Merge')
.addItem('Send Emails', 'sendEmails')
.addToUi();
}
/**
* Send emails from sheet data.
* #param {string} subjectLine (optional) for the email draft message
* #param {Sheet} sheet to read data from
*/
function sendEmails(subjectLine, sheet=SpreadsheetApp.getActiveSheet()) {
// option to skip browser prompt if you want to use this code in other projects
if (!subjectLine){
subjectLine = Browser.inputBox("Mail Merge",
"Type or copy/paste the subject line of the Gmail " +
"draft message you would like to mail merge with:",
Browser.Buttons.OK_CANCEL);
if (subjectLine === "cancel" || subjectLine == ""){
// if no subject line finish up
return;
}
}
// get the draft Gmail message to use as a template
const emailTemplate = getGmailTemplateFromDrafts_(subjectLine);
// get the data from the passed sheet
const dataRange = sheet.getDataRange();
// Fetch displayed values for each row in the Range HT Andrew Roberts
// https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490
// #see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues
const data = dataRange.getDisplayValues();
// assuming row 1 contains our column headings
const heads = data.shift();
// get the index of column named 'Email Status' (Assume header names are unique)
// #see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions
const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL);
// convert 2d array into object array
// #see https://stackoverflow.com/a/22917499/1027723
// for pretty version see https://mashe.hawksey.info/?p=17869/#comment-184945
const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {})));
// used to record sent emails
const out = [];
// loop through all the rows of data
obj.forEach(function(row, rowIdx){
// only send emails is email_sent cell is blank and not hidden by filter
if (row[EMAIL_SENT_COL] == ''){
try {
const msgObj = fillInTemplateFromObject_(emailTemplate.message, row);
// #see https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)
// if you need to send emails with unicode/emoji characters change GmailApp for MailApp
// Uncomment advanced parameters as needed (see docs for limitations)
GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {
htmlBody: msgObj.html,
// bcc: 'a.bbc#email.com',
// cc: 'a.cc#email.com',
from: 'myemail#email.co.uk',
name: 'Custom From Name',
// replyTo: 'a.reply#email.com',
// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)
attachments: emailTemplate.attachments,
inlineImages: emailTemplate.inlineImages
});
// modify cell to record email sent date
out.push([new Date()]);
} catch(e) {
// modify cell to record error
out.push([e.message]);
}
} else {
out.push([row[EMAIL_SENT_COL]]);
}
});
function getccs() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
return sh.getRange(2,4,sh.getLastRow()-1,1).getValues().flat().join(',');
}
// updating the sheet with new data
sheet.getRange(2, emailSentColIdx+1, out.length).setValues(out);
/**
* Get a Gmail draft message by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} containing the subject, plain and html message body and attachments
*/
function getGmailTemplateFromDrafts_(subject_line){
try {
// get drafts
const drafts = GmailApp.getDrafts();
// filter the drafts that match subject line
const draft = drafts.filter(subjectFilter_(subject_line))[0];
// get the message object
const msg = draft.getMessage();
// Handling inline images and attachments so they can be included in the merge
// Based on https://stackoverflow.com/a/65813881/1027723
// Get all attachments and inline image attachments
const allInlineImages = draft.getMessage().getAttachments({includeInlineImages: true,includeAttachments:false});
const attachments = draft.getMessage().getAttachments({includeInlineImages: false});
const htmlBody = msg.getBody();
// Create an inline image object with the image name as key
// (can't rely on image index as array based on insert order)
const img_obj = allInlineImages.reduce((obj, i) => (obj[i.getName()] = i, obj) ,{});
//Regexp to search for all img string positions with cid
const imgexp = RegExp('<img.*?src="cid:(.*?)".*?alt="(.*?)"[^\>]+>', 'g');
const matches = [...htmlBody.matchAll(imgexp)];
//Initiate the allInlineImages object
const inlineImagesObj = {};
// built an inlineImagesObj from inline image matches
matches.forEach(match => inlineImagesObj[match[1]] = img_obj[match[2]]);
return {message: {subject: subject_line, text: msg.getPlainBody(), html:htmlBody},
attachments: attachments, inlineImages: inlineImagesObj };
} catch(e) {
throw new Error("Oops - can't find Gmail draft");
}
/**
* Filter draft objects with the matching subject linemessage by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} GmailDraft object
*/
function subjectFilter_(subject_line){
return function(element) {
if (element.getMessage().getSubject() === subject_line) {
return element;
}
}
}
}
/**
* Fill template string with data object
* #see https://stackoverflow.com/a/378000/1027723
* #param {string} template string containing {{}} markers which are replaced with data
* #param {object} data object used to replace {{}} markers
* #return {object} message replaced with data
*/
function fillInTemplateFromObject_(template, data) {
// we have two templates one for plain text and the html body
// stringifing the object means we can do a global replace
let template_string = JSON.stringify(template);
// token replacement
template_string = template_string.replace(/{{[^{}]+}}/g, key => {
return escapeData_(data[key.replace(/[{}]+/g, "")] || "");
});
return JSON.parse(template_string);
}
/**
* Escape cell data to make JSON safe
* #see https://stackoverflow.com/a/9204218/1027723
* #param {string} str to escape JSON special characters from
* #return {string} escaped string
*/
function escapeData_(str) {
return str
.replace(/[\\]/g, '\\\\')
.replace(/[\"]/g, '\\\"')
.replace(/[\/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t');
};
}
Thanks for any advice.
Get cc's:
function getccs() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('sheetname');
return sh.getRange(2,4,sh.getLastRow()-1,1).getValues().flat().join(',');
}
try it here:
GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {
htmlBody: msgObj.html,
// bcc: 'a.bbc#email.com',
cc: getccs(),
from: 'myemail#email.co.uk',
name: 'Custom From Name',
// replyTo: 'a.reply#email.com',
// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)
attachments: emailTemplate.attachments,
inlineImages: emailTemplate.inlineImages
});
I found a neater way by adding the 3rd line for the CC column:
const RECIPIENT_COL = "E-mail Address";
const EMAIL_SENT_COL = "Email Sent";
const CC_COL = "CC";
And modify sendEmail:
GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {
htmlBody: msgObj.html,
// bcc: 'a.bbc#email.com',
cc: row[CC_COL],
// from: 'an.alias#email.com',
// name: 'name of the sender',
// replyTo: 'a.reply#email.com',
// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)
attachments: emailTemplate.attachments,
inlineImages: emailTemplate.inlineImages
});

cc not working mail merge (google scripts in google sheets), am beginner

So, basically I am writing some code for a teacher to be able to mail student their grades while cc-ing their parents. I don't really know what's happening, but it only cc one parent...
Here's the code, most of it is basic google code that was copied and pasted in, I added lines 25-26 to account for the parent emails, and I uncommented the cc codes on line 94 and added what I think would be correct (I don't code much, I don't even know what language this is in)
What do I have to do to fix this?
// Copyright Martin Hawksey 2020
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
/**
* #OnlyCurrentDoc
*/
/**
* Change these to match the column names you are using for email
* recepient addresses and email sent column.
*/
const RECIPIENT_COL = "Recipient";
const EMAIL_SENT_COL = "Email Sent";
const RECIPIENT2_COL = "Parent Email #1";
const RECIPIENT3_COL = "Parent Email #2";
/**
* Creates the menu item "Mail Merge" for user to run scripts on drop-down.
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Mail Merge')
.addItem('Send Emails', 'sendEmails')
.addToUi();
}
/**
* Send emails from sheet data.
* #param {string} subjectLine (optional) for the email draft message
* #param {Sheet} sheet to read data from
*/
function sendEmails(subjectLine, sheet=SpreadsheetApp.getActiveSheet()) {
// option to skip browser prompt if you want to use this code in other projects
if (!subjectLine){
subjectLine = Browser.inputBox("Mail Merge",
"Type or copy/paste the subject line of the Gmail " +
"draft message you would like to mail merge with:",
Browser.Buttons.OK_CANCEL);
if (subjectLine === "cancel" || subjectLine == ""){
// if no subject line finish up
return;
}
}
// get the draft Gmail message to use as a template
const emailTemplate = getGmailTemplateFromDrafts_(subjectLine);
// get the data from the passed sheet
const dataRange = sheet.getDataRange();
// Fetch displayed values for each row in the Range HT Andrew Roberts
// https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490
// #see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues
const data = dataRange.getDisplayValues();
// assuming row 1 contains our column headings
const heads = data.shift();
// get the index of column named 'Email Status' (Assume header names are unique)
// #see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions
const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL);
// convert 2d array into object array
// #see https://stackoverflow.com/a/22917499/1027723
// for pretty version see https://mashe.hawksey.info/?p=17869/#comment-184945
const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {})));
// used to record sent emails
const out = [];
// loop through all the rows of data
obj.forEach(function(row, rowIdx){
// only send emails is email_sent cell is blank and not hidden by filter
if (row[EMAIL_SENT_COL] == ''){
try {
const msgObj = fillInTemplateFromObject_(emailTemplate.message, row);
// #see https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)
// if you need to send emails with unicode/emoji characters change GmailApp for MailApp
// Uncomment advanced parameters as needed (see docs for limitations)
GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {
htmlBody: msgObj.html,
// bcc: 'a.bbc#email.com',
cc: (row[RECIPIENT2_COL],row[RECIPIENT3_COL]),
// from: 'an.alias#email.com',
// name: 'name of the sender',
// replyTo: 'a.reply#email.com',
// noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)
attachments: emailTemplate.attachments
});
// modify cell to record email sent date
out.push([new Date()]);
} catch(e) {
// modify cell to record error
out.push([e.message]);
}
} else {
out.push([row[EMAIL_SENT_COL]]);
}
});
// updating the sheet with new data
sheet.getRange(2, emailSentColIdx+1, out.length).setValues(out);
/**
* Get a Gmail draft message by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} containing the subject, plain and html message body and attachments
*/
function getGmailTemplateFromDrafts_(subject_line){
try {
// get drafts
const drafts = GmailApp.getDrafts();
// filter the drafts that match subject line
const draft = drafts.filter(subjectFilter_(subject_line))[0];
// get the message object
const msg = draft.getMessage();
// getting attachments so they can be included in the merge
const attachments = msg.getAttachments();
return {message: {subject: subject_line, text: msg.getPlainBody(), html:msg.getBody()},
attachments: attachments};
} catch(e) {
throw new Error("Oops - can't find Gmail draft");
}
/**
* Filter draft objects with the matching subject linemessage by matching the subject line.
* #param {string} subject_line to search for draft message
* #return {object} GmailDraft object
*/
function subjectFilter_(subject_line){
return function(element) {
if (element.getMessage().getSubject() === subject_line) {
return element;
}
}
}
}
/**
* Fill template string with data object
* #see https://stackoverflow.com/a/378000/1027723
* #param {string} template string containing {{}} markers which are replaced with data
* #param {object} data object used to replace {{}} markers
* #return {object} message replaced with data
*/
function fillInTemplateFromObject_(template, data) {
// we have two templates one for plain text and the html body
// stringifing the object means we can do a global replace
let template_string = JSON.stringify(template);
// token replacement
template_string = template_string.replace(/{{[^{}]+}}/g, key => {
return escapeData_(data[key.replace(/[{}]+/g, "")] || "");
});
return JSON.parse(template_string);
}
/**
* Escape cell data to make JSON safe
* #see https://stackoverflow.com/a/9204218/1027723
* #param {string} str to escape JSON special characters from
* #return {string} escaped string
*/
function escapeData_(str) {
return str
.replace(/[\\]/g, '\\\\')
.replace(/[\"]/g, '\\\"')
.replace(/[\/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t');
};
}
Issue:
You are expecting to have multiple emails in cc list but current code will only read and put the value of row[RECIPIENT2_COL].
Solution:
Please try rewriting line 94 of your code to:
cc: row[RECIPIENT2_COL] + "," + row[RECIPIENT3_COL],

Creating Gmail draft with API and include link from Google Sheets script

I have set up a Gsheet in which there are several Email for which drafts need to be created. I use the following code to achieve this:
const RECIPIENT_COL = "Recipient";
const CC= "Sidekick email address";
const DRAFT_CREATED_COL = "Draft created";
const SUBJECT_COL="Subject";
const TEXT="Text";
/**
* Creates the menu item "Mail Merge" for user to run scripts on drop-down.
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Create Drafts')
.addItem('Create Drafts', 'createDraft')
.addToUi();
}
function createDraft(sheet=SpreadsheetApp.getActiveSheet()) {
const dataRange = sheet.getDataRange();
// Fetch displayed values for each row in the Range HT Andrew Roberts
// https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490
// #see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues
const data = dataRange.getDisplayValues();
// assuming row 1 contains our column headings
const heads = data.shift();
// get the index of column named 'Email Status' (Assume header names are unique)
// #see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions
const emailSentColIdx = heads.indexOf(DRAFT_CREATED_COL);
// convert 2d array into object array
// #see https://stackoverflow.com/a/22917499/1027723
// for pretty version see https://mashe.hawksey.info/?p=17869/#comment-184945
const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {})));
// used to record sent emails
const out = [];
// loop through all the rows of data
obj.forEach(function(row, rowIdx){
// only send emails is email_sent cell is blank and not hidden by filter
if (row[DRAFT_CREATED_COL] == ''){
try {
// #see https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)
GmailApp.createDraft(row[RECIPIENT_COL], row[SUBJECT_COL], row[TEXT], {
//htmlBody: emailHtml,
cc: row[CC],
});
// modify cell to record email draft created date
out.push([new Date()]);
} catch(e) {
// modify cell to record error
out.push([e.message]);
}
} else {
out.push([row[DRAFT_CREATED_COL]]);
}
});
// updating the sheet with new data
sheet.getRange(2, emailSentColIdx+1, out.length).setValues(out);
}
However, this does not include the links you can find in the GSheet I use, which you can find here: https://docs.google.com/spreadsheets/d/1hCcEr7L3ejGZJihJ5TPxwX8XMHd0jomHK22zhA5-0y8/edit?usp=sharing
I thought about changing the respective section to the following code section:
emailText=msg.getPlainBody();
emailHtml=msg.getBody();
GmailApp.createDraft(row[RECIPIENT_COL], row[SUBJECT_COL], emailText, {
htmlBody: emailHtml,
Note that this throws the error msg.getPlainBody is not a function. How could I fix this or how could I include the links (maybe with correct html formatting)?

Send a merge mail to a single recipient when a checkbox is checked with google sheets

I have a spreadsheet which is open for editing from you.
https://docs.google.com/spreadsheets/d/1qtUFXNZxvQL2Q4Pljw3NTk3Rr6EB5U5QPCJ_l9mQVY8/edit?usp=sharing
I would like it to send out a merge email to recipient in column O with a merge from column C -> M.
This should be done when a checkbox in column A is checked.
I have made a script with help from this page https://spreadsheet.dev/mail-merge-in-google-sheets which allows me to send to all recipients in column O.
But not only to the one in the row which is checked in column A.
/**
* Returns a rectangular grid of values in a given sheet.
* #param {string} sheetName The name of the sheet object that contains the data to be processed
* #return {object[][]} A two-dimensional array of values in the sheet.
*/
function getData(sheetName) {
var data = SpreadsheetApp.getActive().getSheetByName(sheetName).getDataRange().getValues();
return data;
}
/**
* Renders a template with values from an object.
* #param {string} template The template to render.
* #param{object} data The object containing data to render the template.
* #return {string} The rendered template.
*/
function renderTemplate(template, data) {
var output = template;
var params = template.match(/\{\{(.*?)\}\}/g);
params.forEach(function (param) {
var propertyName = param.slice(2,-2); //Remove the {{ and the }}
output = output.replace(param, data[propertyName] || "");
});
return output;
}
/**
* Converts a grid of values into an array of objects.
* #param {object[][]} rows An array of rows in the grid.
* #return {object[]} An array of objects (each row became an object).
*/
function rowsToObjects(rows) {
var headers = rows.shift();
var data = [];
rows.forEach(function (row) {
var object = {};
row.forEach(function (value, index) {
object[headers[index]] = value;
});
data.push(object);
});
return data;
}
/**
* Sends an email for each row.
*/
function sendEmails() {
var templateData = getData("Templates");
var emailSubjectTemplate = templateData[1][0]; //Cell A2
var emailBodyTemplate = templateData[4][0]; //Cell A5
var emailData = getData("Data");
emailData = rowsToObjects(emailData);
emailData.forEach(function (rowObject) {
var subject = renderTemplate(emailSubjectTemplate, rowObject);
var body = renderTemplate(emailBodyTemplate, rowObject);
MailApp.sendEmail(rowObject["Email"], subject, body);
});
}
Any idea how I can achieve this?
Add an if block in the forEach loop.
You need to modify the sendEmails() function:
function sendEmails() {
var templateData = getData("Templates");
var emailSubjectTemplate = templateData[1][0]; //Cell A2
var emailBodyTemplate = templateData[4][0]; //Cell A5
var emailData = getData("Data");
emailData = rowsToObjects(emailData);
emailData.forEach(function (rowObject) {
if (rowObject["Sendt mail bekræftelse"] == true) { // NEW LINE
var subject = renderTemplate(emailSubjectTemplate, rowObject);
var body = renderTemplate(emailBodyTemplate, rowObject);
MailApp.sendEmail(rowObject["Email"], subject, body);
} // NEW LINE
});
}
Explanation:
The check boxes in your sheet are true or false when read by Apps Script. So within the forEach loop that sends each email based on each row (the rowObject), you can wrap all the actions in an if block to confirm if the check box contains a [✓], which is read as true , that is, (rowObject["Sendt mail bekræftelse"] == true).
Your onEdit trigger should also work now if it is bound to the sendEmails function. Every time a change is made in the sheet, it will check all the checkboxes and if there is a [✓], it will send.
Further reading:
In case its helpful, here are some references on the if...else syntax and forEach.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
Main concepts to grasp here are "conditional statements" and "for loops", in case you want to do some Googling.
Not sure if that answers your question but you can set an onEdit trigger to run .getRange("A2:A").getValues() for each cells in the range. If equal to TRUE (the value for a checked box) and the next is empty, then for app-script to send an email?