From 2f2e6a69ca9c90fb68e60d7a7e00f05f871bba80 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Fri, 8 May 2020 00:17:10 +0900 Subject: [PATCH 1/7] Minor revisions of README for clarification --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3cc507a..7fdf0ec 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Create a Gmail draft to serve as the template. By default, the merge fields are #### Nested Merge In a case where there are two or more entries in your list with the same recipient, you might want to combine the entries into a single email rather than sending the recipient similar emails more than once. Nested merge enables you to specify which field to list individually and which to combine in an email, as shown in the example below. -The nested merge field is, by default, marked by double square brackets, i.e., `[[Meeting ID: {{Meeting ID}}]]`. The merge fields (the curly brackets) nested inside this nested merge field will be merged reclusively if there are two or more rows for the same recipient. A special index field `{{i}}` can be used inside the nested merge field to indicate the index number within the nested merge. To enable the nested merge function, change the value of `ENABLE_NESTED_MERGE`. +The nested merge field is, by default, marked by double square brackets, i.e., `[[Meeting ID: {{Meeting ID}}]]`. The merge fields (the curly brackets) nested inside this nested merge field will be merged reclusively if there are two or more rows for the same recipient. A special index field `{{i}}` can be used inside the nested merge field to indicate the index number within the nested merge. To enable the nested merge function, change the value of `ENABLE_NESTED_MERGE` to `true`. #### Notes - The subject of the template Gmail draft must be unique. An error will be returned during the process of Step 4 below if there are two or more Gmail templates with the designated subject. @@ -99,7 +99,7 @@ We look forward to seeing you! ``` ## Advanced Settings -- The marker for merge fields and nested merge fields can be adjusted via the values `MERGE_FIELD_MARKER` and `NESTED_FIELD_MARKER`, respectively, in the sheet `Config`. You will need to be familiar with the regular expressions of Javascript. +- The markers for merge fields and nested merge fields can be adjusted via the values `MERGE_FIELD_MARKER` and `NESTED_FIELD_MARKER`, respectively, in the sheet `Config`. You will need to be familiar with [the regular expressions of JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). - The index field marker for nested merge `{{i}}` can also be modified through the value `ROW_INDEX_MARKER` in sheet `Config`. - If HTML is enabled in your Gmail, make sure that your modified markers can still be detected in the HTML string. From 7183fdb1bcbf22e8e8309a14fff0094979617d16 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Fri, 8 May 2020 00:41:54 +0900 Subject: [PATCH 2/7] Minor fixes and comment additions --- src/mailMerge.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mailMerge.js b/src/mailMerge.js index 31fe785..87036fe 100644 --- a/src/mailMerge.js +++ b/src/mailMerge.js @@ -73,7 +73,7 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { : `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.'); + throw new Error('Mail merge canceled.'); } // Get template from Gmail draft let promptMessage = 'Enter the subject text of Gmail draft to use as template.'; @@ -85,17 +85,16 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { throw new Error('No text entered.'); } let draftMessage = getDraftBySubject_(subjectText); - //// Check for duplicates + // Check for duplicates if (draftMessage.length > 1) { throw new Error('There are 2 or more Gmail drafts with the subject you entered. Enter a unique subject text.'); } - //// Store template into an object + // Store template into an object let template = { 'subject': subjectText, 'plainBody': draftMessage[0].getPlainBody(), 'htmlBody': draftMessage[0].getBody() }; - // Check for consistency between config.ENABLE_NESTED_MERGE and template if (config.ENABLE_NESTED_MERGE === false) { let nmFieldCounter = 0; @@ -104,13 +103,16 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { let nmFieldCount = (nmField === null ? 0 : nmField.length) nmFieldCounter += nmFieldCount; } + // If nested merge field marker is detected in the template when ENABLE_NESTED_MERGE is set to false, + // ask whether or not to enable this function, i.e., to change ENABLE_NESTED_MERGE to true. if (nmFieldCounter > 0){ let confirmNM = 'Nested merge field marker detected. Do you want to enable nested merge function?'; let result = ui.alert('Confirmation', confirmNM, ui.ButtonSet.YES_NO); config.ENABLE_NESTED_MERGE = (result === ui.Button.YES ? true : config.ENABLE_NESTED_MERGE); } } - + // Create draft or send email based on the template. + // The process depends on the value of ENABLE_NESTED_MERGE if (config.ENABLE_NESTED_MERGE === true) { // Convert the 2d-array merge data into object grouped by recipient(s) let groupedMergeData = groupArray_(mergeData, config.RECIPIENT_COL_NAME); From 262f2b4ee5463c42c2ca23415c8a9f72f2240675 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Fri, 8 May 2020 01:43:57 +0900 Subject: [PATCH 3/7] Minor fixes in order of functions --- src/mailMerge.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/mailMerge.js b/src/mailMerge.js index 87036fe..e71f11a 100644 --- a/src/mailMerge.js +++ b/src/mailMerge.js @@ -35,19 +35,19 @@ function onOpen() { } /** - * Send personalized email(s) to recipient address listed in sheet 'SHEET_NAME_DATA' + * Send test emails to myself the content of the first row in sheet 'List' */ -function sendEmails() { - const draftMode = false; +function createDraftEmails() { + const draftMode = true; const config = getConfig_('Config'); sendPersonalizedEmails_(draftMode, config); } /** - * Send test emails to myself the content of the first row in sheet 'List' + * Send personalized email(s) to recipient address listed in sheet 'SHEET_NAME_DATA' */ -function createDraftEmails() { - const draftMode = true; +function sendEmails() { + const draftMode = false; const config = getConfig_('Config'); sendPersonalizedEmails_(draftMode, config); } @@ -199,6 +199,17 @@ 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; +} + /** * Create a Javascript object from a 2d array, grouped by a given property. * @param {array} data 2-dimensional array with a header as its first row. @@ -253,17 +264,6 @@ function createObj_(keys, values) { return obj; } -/** - * 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 mergeFieldMarker @@ -299,7 +299,7 @@ function fillInTemplate_(template, data, replaceValue = 'NA', mergeFieldMarker = let datum = data[i]; let rowIndex = i + 1; let fieldRowIndexed = field.replace(rowIndexMarker, rowIndex); - let fieldVarsCopy = fieldVars; + let fieldVarsCopy = fieldVars.slice(); // Get the text inside markers, e.g., {{field name}} => field name let fieldMarkerText = fieldVarsCopy.map(value => value.substring(2, value.length - 2)); // assuming that the text length for opening and closing markers are 2 and 2, respectively fieldVarsCopy.forEach( From 71dacc71c4cbff7b6dc1bab85e414c69e7872e75 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Fri, 8 May 2020 01:45:44 +0900 Subject: [PATCH 4/7] Added arrReplace_() for future reference --- src/mailMerge.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/mailMerge.js b/src/mailMerge.js index e71f11a..2c7a8ec 100644 --- a/src/mailMerge.js +++ b/src/mailMerge.js @@ -345,3 +345,15 @@ function errorMessage_(e) { let message = `Error: line - ${e.lineNumber}\n[${e.name}] ${e.message}\n${e.stack}` return message; } + +/** + * Process each element of an array with String.prototype.replace() + * @param {array} array Array containing values to replace. + * @param {string|RegExp} searchValue A RegExp object or literal, or string to be replaced by replaceValue + * @param {string} replaceValue String to replace the searchValue. + * @return {array} Array whose element(s) are replaced. + */ +function arrReplace_(array, searchValue, replaceValue) { + let replacedArray = array.map(value => value.replace(searchValue, replaceValue)); + return replacedArray; +} From 1aa621082b459ac9f748d5f1cde7ba7e860a9c2f Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Sun, 10 May 2020 00:53:51 +0900 Subject: [PATCH 5/7] Inserted variable mergedDataEolReplaced --- src/mailMerge.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mailMerge.js b/src/mailMerge.js index 2c7a8ec..2324107 100644 --- a/src/mailMerge.js +++ b/src/mailMerge.js @@ -62,10 +62,16 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { var ss = SpreadsheetApp.getActiveSpreadsheet(); var ui = SpreadsheetApp.getUi(); var myEmail = Session.getActiveUser().getEmail(); + // Get data of field(s) to merge in form of 2d array var dataSheet = ss.getSheetByName(config.DATA_SHEET_NAME); var mergeDataRange = dataSheet.getDataRange().setNumberFormat('@'); // Convert all formatted dates and numbers into texts var mergeData = mergeDataRange.getValues(); + + // Convert line breaks in the spreadsheet (in LF format, i.e., '\n') + // to CRLF format ('\r\n') for merging into Gmail plain text + let mergeDataEolReplaced = mergeData.map(element => arrReplace_(element, /\n|\r|\r\n/g, '\r\n')); + try { // Confirmation before sending email let confirmAccount = (draftMode === true @@ -115,7 +121,7 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { // The process depends on the value of ENABLE_NESTED_MERGE if (config.ENABLE_NESTED_MERGE === true) { // Convert the 2d-array merge data into object grouped by recipient(s) - let groupedMergeData = groupArray_(mergeData, config.RECIPIENT_COL_NAME); + let groupedMergeData = groupArray_(mergeDataEolReplaced, config.RECIPIENT_COL_NAME); // Validity check if (Object.keys(groupedMergeData).length == 0) { throw new Error('Invalid RECIPIENT_COL_NAME. Check sheet "Config" to make sure it refers to an existing column name.'); @@ -134,7 +140,7 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { } } else { // Convert the 2d-array merge data into object - let groupedMergeData = groupArray_(mergeData); + let groupedMergeData = groupArray_(mergeDataEolReplaced); // Create draft or send email for each recipient for (let i = 0; i < groupedMergeData.data.length; ++i) { let object = groupedMergeData.data[i] From e20104ae5d8997a5108075cac85e175a91b98a62 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Sun, 10 May 2020 01:17:11 +0900 Subject: [PATCH 6/7] Organized order of functions for better readability --- src/mailMerge.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/mailMerge.js b/src/mailMerge.js index 2324107..c6302ba 100644 --- a/src/mailMerge.js +++ b/src/mailMerge.js @@ -167,6 +167,18 @@ function sendPersonalizedEmails_(draftMode = true, config = CONFIG) { } } +/** + * Process each element of an array with String.prototype.replace() + * @param {array} array Array containing values to replace. + * @param {string|RegExp} searchValue A RegExp object or literal, or string to be replaced by replaceValue + * @param {string} replaceValue String to replace the searchValue. + * @return {array} Array whose element(s) are replaced. + */ +function arrReplace_(array, searchValue, replaceValue) { + let replacedArray = array.map(value => value.replace(searchValue, replaceValue)); + return replacedArray; +} + /** * Returns an object of configurations from spreadsheet. * @param {string} configSheetName Name of sheet with configurations. Defaults to 'Config'. @@ -351,15 +363,3 @@ function errorMessage_(e) { let message = `Error: line - ${e.lineNumber}\n[${e.name}] ${e.message}\n${e.stack}` return message; } - -/** - * Process each element of an array with String.prototype.replace() - * @param {array} array Array containing values to replace. - * @param {string|RegExp} searchValue A RegExp object or literal, or string to be replaced by replaceValue - * @param {string} replaceValue String to replace the searchValue. - * @return {array} Array whose element(s) are replaced. - */ -function arrReplace_(array, searchValue, replaceValue) { - let replacedArray = array.map(value => value.replace(searchValue, replaceValue)); - return replacedArray; -} From 78bdc411bdf32d8203a9990aaef4777c4de9315a Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Sun, 10 May 2020 01:17:49 +0900 Subject: [PATCH 7/7] Added a note on line breaks within spreadsheet --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7fdf0ec..4616b6e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Edit your spreadsheet in any way you want to. If you want to change the sheet na - The default value for the field (column) name of recipient email address is set to `Email`; change the value of `RECIPIENT_COL_NAME` in sheet `Config` to suit your needs - Changing sheet name of `Config` is not recommended unless you are familiar with Google Apps Script and can edit the relevant section of the script. - The lower-case letter `i` is reserved as part of the nested merge function, as described below, and cannot be used for a column name. +- Line breaks within a spreadsheet cell will be reflected in the plain text version of the merged mail, but not in the HTML version. ### 3. Create a template draft on Gmail Create a Gmail draft to serve as the template. By default, the merge fields are specified by double curly brackets, i.e., `Dear {{Name}},... `. The field names should correspond with the column names of the spreadsheet (case-sensitive). If HTML mail is enabled, text styles of the draft template will be reflected on the personalized emails.