-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from ttsukagoshi/dev202004
Added mergeMail.gs
- Loading branch information
Showing
1 changed file
with
196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
// Add spreadsheet menu | ||
function onOpen() { | ||
let ui = SpreadsheetApp.getUi(); | ||
ui.createMenu('Mail Merge') | ||
.addItem('Send Emails', 'sendEmails') | ||
.addSeparator() | ||
.addItem('Create Draft', 'createDraftEmails') | ||
.addToUi(); | ||
} | ||
|
||
/** | ||
* Send personalized email(s) to recipient address listed in sheet 'SHEET_NAME_DATA' | ||
*/ | ||
function sendEmails() { | ||
const draftMode = false; | ||
const config = getConfig_('Config'); | ||
sendPersonalizedEmails_(draftMode, config); | ||
} | ||
|
||
/** | ||
* Send test emails to myself the content of the first row in sheet 'List' | ||
*/ | ||
function createDraftEmails() { | ||
const draftMode = true; | ||
const config = getConfig_('Config'); | ||
sendPersonalizedEmails_(draftMode, config); | ||
} | ||
|
||
/** | ||
* Bulk send personalized emails based on a designated Gmail draft. | ||
* The email(s) can be sent to multiple recipients, which will serve as an alternative for using BCC. | ||
* @param {boolean} draftMode Creates Gmail draft(s) instead of sending email. Defaults to true. | ||
* @param {Object} config Object returned by getConfig_() | ||
*/ | ||
function sendPersonalizedEmails_(draftMode = true, config) { | ||
var ss = SpreadsheetApp.getActiveSpreadsheet(); | ||
var ui = SpreadsheetApp.getUi(); | ||
var myEmail = Session.getActiveUser().getEmail(); | ||
|
||
// Get data of field(s) to merge | ||
//// Range of data | ||
const dataSheet = ss.getSheetByName(config.DATA_SHEET_NAME); | ||
var mergeDataRange = dataSheet.getDataRange().setNumberFormat('@'); // Convert all formatted dates and numbers into texts | ||
//// Get data | ||
var mergeData = mergeDataRange.getValues(); | ||
//// Define first row of mergeData as header | ||
var header = mergeData.shift(); | ||
//// Convert 2d array of mergeData into object array | ||
var mergeDataObj = mergeData.map(function (values) { | ||
return header.reduce(function (object, key, index) { | ||
object[key] = values[index]; | ||
return object; | ||
}, {}); | ||
}) | ||
|
||
try { | ||
// Confirmation before sending email | ||
let confirmAccount = (draftMode === true | ||
? `Are you sure you want to create draft email(s) as ${myEmail}?` | ||
: `Are you sure you want to send email(s) as ${myEmail}?`); | ||
let answer = ui.alert(confirmAccount, ui.ButtonSet.OK_CANCEL); | ||
if (answer !== ui.Button.OK) { | ||
throw new Error('Canceled.'); | ||
} | ||
|
||
// Email Template | ||
//// Prompt to enter the subject of the Gmail draft to use as template | ||
let promptMessage = 'Enter the subject text of Gmail draft to use as template.'; | ||
let promptResult = ui.prompt(promptMessage, ui.ButtonSet.OK_CANCEL); | ||
let [selectedButton, subjectText] = [promptResult.getSelectedButton(), promptResult.getResponseText()]; | ||
if (selectedButton !== ui.Button.OK) { | ||
throw new Error('Canceled.'); | ||
} else if (subjectText == null) { | ||
throw new Error('No text entered.'); | ||
} | ||
//// Get template | ||
let draftMessage = getDraftBySubject_(subjectText); | ||
if (draftMessage.length > 1) { | ||
throw new Error('There are 2 or more Gmail drafts with the subject you entered. Enter a unique subject text.'); | ||
} | ||
var template = { | ||
'subject': subjectText, | ||
'plainBody': draftMessage[0].getPlainBody(), | ||
'htmlBody': draftMessage[0].getBody() | ||
}; | ||
|
||
// Send or create draft of personalized email | ||
mergeDataObj.forEach((element, i) => { | ||
var messageData = fillInTemplateFromObject_(template, element, config.MERGE_FIELD_MARKER, config.REPLACE_VALUE); | ||
var options = { | ||
'htmlBody': messageData.htmlBody, | ||
'bcc': (config.BCC_TO_MYSELF === true ? myEmail : null) | ||
}; | ||
draftMode === true | ||
? GmailApp.createDraft(element[config.RECIPIENT_COL_NAME], messageData.subject, messageData.plainBody, options) | ||
: GmailApp.sendEmail(element[config.RECIPIENT_COL_NAME], messageData.subject, messageData.plainBody, options); | ||
}); | ||
let completeMessage = (draftMode === true | ||
? 'Complete: All draft(s) created.' | ||
: 'Complete: All mails sent.'); | ||
ui.alert(completeMessage) | ||
} catch (e) { | ||
let message = errorMessage_(e); | ||
ui.alert(message); | ||
} | ||
} | ||
|
||
/** | ||
* Returns an object of configurations from spreadsheet. | ||
* The sheet should have a first row of headers, and its first column should include the following properties: | ||
* DATA_SHEET_NAME: {string} Name of sheet in which field(s) to merge in email are stored | ||
* RECIPIENT_COL_NAME: {string} Name of column in sheet 'DATA_SHEET_NAME' that designates the email address of the recipient | ||
* BCC_TO_MYSELF: {string} String boolean. When true, will send (or create draft) email with the sender's address set to BCC. | ||
* REPLACE_VALUE: {string} Text that will replace empty data of marker. | ||
* MERGE_FIELD_MARKER: {string} Text to be processed in RegExp() constructor to define merge field marker. Note that the backslash itself does not need to be escaped, i.e., does not need to be repeated. | ||
* | ||
* @param {string} configSheetName Name of sheet with configurations. Defaults to 'Config'. | ||
* @return {Object} | ||
*/ | ||
function getConfig_(configSheetName = 'Config') { | ||
// Get values from spreadsheet | ||
let configValues = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(configSheetName).getDataRange().getValues(); | ||
configValues.shift(); | ||
|
||
// Convert the 2d array values into a Javascript object | ||
let configObj = {}; | ||
configValues.forEach(element => configObj[element[0]] = element[1]); | ||
|
||
// Convert data types | ||
configObj.BCC_TO_MYSELF = toBoolean_(configObj.BCC_TO_MYSELF); | ||
configObj.MERGE_FIELD_MARKER = new RegExp(configObj.MERGE_FIELD_MARKER, 'g'); | ||
|
||
return configObj; | ||
} | ||
|
||
/** | ||
* Convert string booleans into boolean data | ||
* @param {string} stringBoolean | ||
* @return {boolean} | ||
*/ | ||
function toBoolean_(stringBoolean) { | ||
return stringBoolean.toLowerCase === 'true'; | ||
} | ||
|
||
/** | ||
* Get an array of Gmail message(s) with the designated subject | ||
* @param {string} subject Subject text of Gmail draft | ||
* @return {array} Array of GmailMessage class objects. https://developers.google.com/apps-script/reference/gmail/gmail-message | ||
*/ | ||
function getDraftBySubject_(subject) { | ||
let draftMessages = GmailApp.getDraftMessages(); | ||
let targetDrafts = draftMessages.filter(element => element.getSubject() == subject); | ||
return targetDrafts; | ||
} | ||
|
||
/** | ||
* Replaces markers in a template object with values defined in a JavaScript data object. | ||
* @param {Object} template Template object containing markers, as designated in regular expression in MERGE_FIELD_MARKER | ||
* @param {Object} mergeData Object with values to replace markers. | ||
* @param {string} mergeFieldMarker Regular expression for the merge field marker. Defaults to /\{\{[^\}]+\}\}/g e.g., {{field name}} | ||
* @param {string} replaceValue String to replace empty data of a marker. Defaults to 'NA' for Not Available. | ||
* @return {Object} Returns object with markers replaced. | ||
*/ | ||
function fillInTemplateFromObject_(template, mergeData, mergeFieldMarker = /\{\{[^\}]+\}\}/g, replaceValue = 'NA') { | ||
let messageData = {}; | ||
for (let k in template) { | ||
let text = ''; | ||
text = template[k]; | ||
// Search for all the variables to be replaced | ||
let textVars = text.match(mergeFieldMarker); | ||
if (!textVars) { | ||
messageData[k] = text; // return text itself if no marker is found | ||
} else { | ||
// Get the text inside markers. e.g., {{field name}} => field name | ||
// assuming that the text length for opening and closing markers are 2 and 2, respectively | ||
let markerText = textVars.map(value => value.substring(2, value.length - 2)); | ||
// Replace variables in textVars with the actual values from the data object. | ||
// If no value is available, replace with the string with replaceValue. | ||
textVars.forEach( | ||
(variable, i) => text = text.replace(variable, mergeData[markerText[i]] || replaceValue) | ||
); | ||
messageData[k] = text; | ||
} | ||
} | ||
return messageData; | ||
} | ||
|
||
/** | ||
* Standarized error message | ||
* @param {Object} e Error object returned by try-catch | ||
* @return {string} Standarized error message | ||
*/ | ||
function errorMessage_(e) { | ||
let message = `Error: line - ${e.lineNumber}\n[${e.name}] ${e.message}\n${e.stack}` | ||
return message; | ||
} |