title: Curly brackets to %7 in Gmail
body: <p>I followed <a href="https://developers.google.com/workspace/solutions/mail-merge" rel="nofollow noreferrer">this tutorial</a> 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:</p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>// 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 &amp; 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 =&gt; (heads.reduce((o, k, i) =&gt; (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) =&gt; (obj[i.getName()] = i, obj) ,{});
  
        //Regexp to search for all img string positions with cid
        const imgexp = RegExp('&lt;img.*?src="cid:(.*?)".*?alt="(.*?)"[^\&gt;]+&gt;', 'g');
        const matches = [...htmlBody.matchAll(imgexp)];
  
        //Initiate the allInlineImages object
        const inlineImagesObj = {};
        // built an inlineImagesObj from inline image matches
        matches.forEach(match =&gt; 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 =&gt; {
        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');
    };
  }</code></pre>
  </div>
  </div>
  </p>
  <p>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.</p>
  <p><a href="https://i.stack.imgur.com/ODudQ.png" rel="nofollow noreferrer">Gmail screenshot 1</a>
  <a href="https://i.stack.imgur.com/3MypV.png" rel="nofollow noreferrer">Gmail screenshot 2</a></p>
  <p>I use Stripo to make my template and this is the code:</p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;td class="esd-block-button es-p40t es-p40b es-p25r es-p25l" align="center" bgcolor="#ffffff"&gt;&lt;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;"&gt;&lt;a href="{{Link}}" class="es-button es-button-1621245929191" target="_blank" style="background-color: #234290; border-color: #234290; color: #ffffff; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; border-width: 10px 30px;"&gt;Progressie formulier&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;</code></pre>
  </div>
  </div>
  </p>
  <p>So I included the {{Link}} in the A href.</p>
  <p>My questions:
  Would changing the {{}} to something like this %%%% or this &lt;&lt;&gt;&gt; gmail not changing my merge tag?</p>
  <p>Any other ideas how I can fix this would be awesome!</p>
  <p>Sorry for the bad English btw :)</p>