Photo by Stephen Phillips - Hostreviews.co.uk on Unsplash (https://unsplash.com/photos/3Mhgvrk4tjM?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

How I built my newsletter using Gmail and Google Sheets

By Omar Kamali • March 12, 2023 • In Programming, Hacks, Fun, Blog update

New technologies are emerging by the second, and we didn't even reach the singularity[1] yet - or did we?

Each new technology comes with a lot of promises, making it easy to get swept away by the hype. As a software engineer, staying ahead of the curve is a big part of the profession, however picking the right tool for the job is just as critical. Striking a balance between novelty and proven technology is one of the most impactful factors determining the success of a software project.

In this category of "Old but gold", one gift that keeps on giving is the Google Apps Script platform. Just keep using it for long enough and Google Workspace (Docs, Sheets, Drive ...) starts to feel like an extension for Apps Script rather than the other way around.

I've recently wanted to add a newsletter to my blog, notably because I started writing a series and I thought some readers might want to know when the next post is out.

I have used marketing automation and emailing platforms before, but that felt a bit overkill for this use case. All I want to do is send a periodic update containing the posts I made in that period. And maybe an occasional note alongside that.

Okay I'm an engineer, I can put this together quickly. You probably already see it coming, this will overshoot and take forever and I will end up in a rabbit hole debugging my emailing system instead of writing the actual posts. No. I'm not gonna fall for this one again. NIH[2]  syndrome is insidious.

Don't fall for it

I fell for it. It still took less than a day in total and I ended up with the following flow on the subscription side:

  1. A user enters their name and their email in a box on my website
  2. The user receives a confirmation email (to avoid people subscribing email addresses they don't own)<br>
  3. The confirmation email contains a link to open a confirmation page instead of confirming the subscription immediately on click (to avoid email apps opening the confirmation link accidentally)<br>
  4. A thank you page is displayed.<br>

On the admin side, I have a spreadsheet with the list of subscribed emails, their subscription and confirmation dates, and any notes I might add if it's someone I know.

This is the newsletter spreadsheet at the time of writing. Subscribe now and be the first!

The script is automatically backing up this spreadsheet, itself, and deleting older backups.

On the newsletter side, each email is sent from my gmail address. It also contains an unsubscribe link that opens an unsubscription confirmation page with a button. Clicking on the button will delete the user from the list of emails. They will be completely deleted when the oldest backup containing their data expires.

And this is the confirmation page that you get when you open the confirmation link in your confirmation email. You can never have too many confirmations.

That's it. A completely custom system with very little code in it, so not much to maintain.

As for actually sending the newsletter emails, I'm planning to send personalized emails until the number of subscribers becomes too unwieldy, if ever.

How did I build it?

If you're a Google user (Gmail, Sheets, Drive ..) and this is the first time you hear about Google Apps Script, oh boy are you in for a surprise. It's an automation platform allowing you to orchestrate Google services together programmatically, expose API endpoints (implemented in a serverless-like environment), and even serve HTML content, with zero hosting or deployment overhead.

The only constraint is that it's based around a programming language (almost identical to Javascript). It requires some coding, but once you get past that initial learning curve, some crazy possibilities are open to you. From this point below, I am assuming you have some familiarity with programming so you can follow along. If not, and you have an urgent need for a newsletter, you are better served by commercial providers or whatever your CRM provides.

Back to my newsletter. It's actually a pretty simple script, with Google services doing all the heavy lifting, and a holy spreadsheet connecting it all together.

This is the code for the subscription endpoint my website connects to. Notice how easy it is to read, write from Sheets and to send emails with Gmail:

<span style="font-size: 14px;"><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">/**
  * Subscribes a new user to Omar Kamali's newsletter.
  * </span></span></span><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag">@param </span></span></span></span></span></span><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type">{Object}</span></span></span></span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"> </span></span></span></span></span></span><span class="hljs-variable"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-variable"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-variable"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-variable">subscriber</span></span></span></span></span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment"> - The subscriber object.
  * </span></span></span><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag">@param </span></span></span></span></span></span><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type">{string}</span></span></span></span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"> </span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">subscriber.name - The name of the subscriber.
  * </span></span></span><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag">@param </span></span></span></span></span></span><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type">{string}</span></span></span></span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"> </span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">subscriber.email - The email address of the subscriber.
  * </span></span></span><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag">@return </span></span></span></span></span></span><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-type">{boolean}</span></span></span></span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"><span class="hljs-comment"><span class="hljs-doctag"> </span></span></span></span></span></span></span><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">True if the subscription was successful, false otherwise.
  */</span></span></span></span>
  
<span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword">function</span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"> </span></span></span><span class="hljs-title"><span class="hljs-function"><span class="hljs-title"><span class="hljs-function"><span class="hljs-title"><span class="hljs-function"><span class="hljs-title">subscribe</span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">(</span></span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">{name, email}</span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">) </span></span></span></span>{
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span></span> ss = SpreadsheetApp.getActiveSpreadsheet();
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span></span> sheet = ss.getSheetByName(<span class="hljs-string"><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">"Subscriptions"</span></span></span></span>);

  <span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">// Check if the email is already in the spreadsheet (i.e. subscribed)</span></span></span></span>
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span></span> data = sheet.getDataRange().getValues();
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">for</span></span></span></span> (<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">let</span></span></span></span> i = <span class="hljs-number"><span class="hljs-number"><span class="hljs-number"><span class="hljs-number">1</span></span></span></span>; i &lt; data.length; i++) {
    <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">if</span></span></span></span> (data[i][<span class="hljs-number"><span class="hljs-number"><span class="hljs-number"><span class="hljs-number">1</span></span></span></span>] == email) {
      <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span></span> <span class="hljs-literal"><span class="hljs-literal"><span class="hljs-literal"><span class="hljs-literal">false</span></span></span></span>;
    }
  }

  <span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">// Store a new subscriber entry in the sheet</span></span></span></span>
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span></span> token = generateToken();
  sheet.appendRow([name, email, <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">new</span></span></span></span> <span class="hljs-built_in"><span class="hljs-built_in"><span class="hljs-built_in"><span class="hljs-built_in">Date</span></span></span></span>(), token]);

  <span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">// Send a confirmation email</span></span></span></span>
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span></span> body = <span class="hljs-string"><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">`Thank you for subscribing to my newsletter!

Please click the link below to confirm your subscription:
</span></span></span><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><font color="#98c379"><span class="hljs-string">
</span></font><span class="hljs-string"><span class="hljs-subst">${ScriptApp.getService().getUrl()}</span></span></span></span></span></span></span><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">?token=</span></span></span><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst">${token}</span></span></span></span></span></span></span><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">&amp;p=confirm-subscribe`</span></span></span></span>;

  GmailApp.sendEmail(email, <span class="hljs-string"><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">"Confirm subscribing to Omar Kamali's newsletter"</span></span></span></span>, body);

  <span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment"><span class="hljs-comment">// Send an update to my personal Discord server</span></span></span></span>
  sendDiscordMessage(<span class="hljs-string"><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">"Subscription initiated"</span></span></span></span>, {name, email});

  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span></span> <span class="hljs-literal"><span class="hljs-literal"><span class="hljs-literal"><span class="hljs-literal">true</span></span></span></span>;
}</span>

This function <i>subscribe</i> is triggered when the user submits the form on my website. It saves a record in Sheets and then sends a confirmation email using Gmail.

More secret sauce

You might have noticed that the script is supposed to display multiple pages depending on the context. There are four pages in total:

  1. Subscription page, which you get when you submit the newsletter form below this article.
  2. Subscription confirmation page, which you get when you open the confirmation link sent via email.
  3. Unsubscription confirmation page, which you get when you open the link to unsubscribe in any of my newsletter emails.
  4. The famous 404, if someone somehow hits an unknown page.

In a regular web app context, you would have a router (like express for node.js), with which you can handle different routes such as <i>/subscribe</i>, <i>/confirm</i>, <i>/unsubscribe</i>. A Google Apps Script has only one function named <i>doGet</i> to handle GET requests, and one function named <i>doPost</i> to handle POST requests. So I implemented a sort of routing pattern:

<span style="font-size: 14px;"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword">function</span></span></span><span class="hljs-function"> </span><span class="hljs-title"><span class="hljs-function"><span class="hljs-title">doGet</span></span></span><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">e</span></span></span><span class="hljs-function">) </span></span>{
  <span class="hljs-keyword"><span class="hljs-keyword">let</span></span> content = <span class="hljs-string"><span class="hljs-string">''</span></span>;
  <span class="hljs-keyword"><span class="hljs-keyword">const</span></span> path = e.parameter.p;
  <span class="hljs-keyword"><span class="hljs-keyword">const</span></span> token = e.parameter.token;

  <span class="hljs-keyword"><span class="hljs-keyword">switch</span></span> (path) {
    <span class="hljs-keyword"><span class="hljs-keyword">case</span></span> <span class="hljs-string"><span class="hljs-string">"confirm-subscribe"</span></span>:
      content = render(<span class="hljs-string"><span class="hljs-string">'confirm-subscribe'</span></span>, { token });
      <span class="hljs-keyword"><span class="hljs-keyword">break</span></span>;
    <span class="hljs-keyword"><span class="hljs-keyword">case</span></span> <span class="hljs-string"><span class="hljs-string">"confirm-unsubscribe"</span></span>:
      content = render(<span class="hljs-string"><span class="hljs-string">'confirm-unsubscribe'</span></span>, { token });
      <span class="hljs-keyword"><span class="hljs-keyword">break</span></span>;
    <span class="hljs-keyword"><span class="hljs-keyword">default</span></span>:
      content = render(<span class="hljs-string"><span class="hljs-string">'404'</span></span>);
  }

  <span class="hljs-keyword"><span class="hljs-keyword">return</span></span> HtmlService.createHtmlOutput(content);
}

<span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword">function</span></span></span><span class="hljs-function"> </span><span class="hljs-title"><span class="hljs-function"><span class="hljs-title">doPost</span></span></span><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">e</span></span></span><span class="hljs-function">) </span></span>{
  <span class="hljs-keyword"><span class="hljs-keyword">let</span></span> content = <span class="hljs-string"><span class="hljs-string">''</span></span>;
  <span class="hljs-keyword"><span class="hljs-keyword">const</span></span> path = e.parameter.p;

  <span class="hljs-keyword"><span class="hljs-keyword">switch</span></span> (path) {
    <span class="hljs-keyword"><span class="hljs-keyword">case</span></span> <span class="hljs-string"><span class="hljs-string">"subscribe"</span></span>:
      content = render(<span class="hljs-string"><span class="hljs-string">'subscribe'</span></span>, { <span class="hljs-attr"><span class="hljs-attr">email</span></span>: e.parameter.email, <span class="hljs-attr"><span class="hljs-attr">success</span></span>: subscribe(e.parameter) });
      <span class="hljs-keyword"><span class="hljs-keyword">break</span></span>;
    <span class="hljs-keyword"><span class="hljs-keyword">default</span></span>:
      content = render(<span class="hljs-string"><span class="hljs-string">'404'</span></span>);
      <span class="hljs-keyword"><span class="hljs-keyword">break</span></span>;
  }

  <span class="hljs-keyword"><span class="hljs-keyword">return</span></span> HtmlService.createHtmlOutput(content);
}</span>

As for the render function, it uses Google's own templating engine and some HTML template files to output the HTML for each route. This is what it looks like:

<div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword">function</span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"> </span></span></span></span><span class="hljs-title"><span class="hljs-function"><span class="hljs-title"><span class="hljs-function"><span class="hljs-title"><span class="hljs-function"><span class="hljs-title"><span class="hljs-function"><span class="hljs-title">render</span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">(</span></span></span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">templateName, data</span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">) </span></span></span></span></span>{
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span></span></span> html = HtmlService.createTemplateFromFile(templateName);</span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;">
  html.data  = data;
  
  <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span></span></span> html.evaluate();
</span></font>}</div>

And this is what some of the HTML looks like for the <i>confirm-subscribe</i> route:

<div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-tag"><span class="hljs-tag">&lt;</span><span class="hljs-name"><span class="hljs-tag"><span class="hljs-name">div</span></span></span><span class="hljs-tag"> </span><span class="hljs-attr"><span class="hljs-tag"><span class="hljs-attr">id</span></span></span><span class="hljs-tag">=</span></span><span class="hljs-string"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">"message"</span></span></span></span></span></span><span class="hljs-tag"><span class="hljs-tag">&gt;</span></span>
  Please confirm subscribing to my newsletter.
  <span class="hljs-tag"><span class="hljs-tag">&lt;</span><span class="hljs-name"><span class="hljs-tag"><span class="hljs-name">div</span></span></span><span class="hljs-tag">&gt;</span></span></span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;">    <span class="hljs-tag"><span class="hljs-tag">&lt;</span><span class="hljs-name"><span class="hljs-tag"><span class="hljs-name">button</span></span></span></span></span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-tag"><span class="hljs-tag">      </span><span class="hljs-attr"><span class="hljs-tag"><span class="hljs-attr">onclick</span></span></span><span class="hljs-tag">=</span></span><span class="hljs-string"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">"confirmSubscription('&lt;?= data.token ?&gt;')"</span></span></span></span></span></span></span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-class"><span class="hljs-keyword"><span class="hljs-tag"><span class="hljs-tag">      </span></span><span class="hljs-class"><span class="hljs-keyword"><span class="hljs-tag"><span class="hljs-attr"><span class="hljs-tag"><span class="hljs-attr">class</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-tag">=</span><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">"</span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">bg</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-</span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">blue</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-500 </span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">rounded</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-</span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">lg</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string"> </span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">px</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-4 </span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">py</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-1 </span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">text</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-</span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">white</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string"> </span></span></span></span></span><span class="hljs-title"><span class="hljs-class"><span class="hljs-title"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">mt</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-string"><span class="hljs-tag"><span class="hljs-string">-2"</span></span></span><span class="hljs-tag">&gt;</span></span></span></span></span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-class"><span class="hljs-title"><span class="hljs-class">      <span class="hljs-title">Subscribe</span></span></span></span></span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-class"><span class="hljs-class">    <span class="hljs-tag"><span class="hljs-tag">&lt;</span></span></span><span class="hljs-type"><span class="hljs-class"><span class="hljs-type"><span class="hljs-tag"><span class="hljs-tag">/</span><span class="hljs-name"><span class="hljs-tag"><span class="hljs-name">button</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-tag">&gt;</span></span></span></span></span></font></div><div style="--tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ;"><font color="#c678dd" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><span class="hljs-class"><span class="hljs-class">  <span class="hljs-tag"><span class="hljs-tag">&lt;</span></span></span><span class="hljs-type"><span class="hljs-class"><span class="hljs-type"><span class="hljs-tag"><span class="hljs-tag">/</span><span class="hljs-name"><span class="hljs-tag"><span class="hljs-name">div</span></span></span></span></span></span></span><span class="hljs-class"><span class="hljs-tag"><span class="hljs-tag">&gt;</span></span></span></span>
<span class="hljs-tag"><span class="hljs-tag">&lt;/</span><span class="hljs-name"><span class="hljs-tag"><span class="hljs-name">div</span></span></span><span class="hljs-tag">&gt;</span></span></span></font><font color="#3c4043" face="Roboto Mono, Menlo, Monaco, Courier New, monospace"><span style="font-size: 13px;"><br></span></font></div>

Welp, that's it!

I hope this inspires you to automate some of your workflows on Google Workspace and create completely new functionality and experiences for your users using this pretty simple and powerful technology.<br>

You can start your journey with Google Apps Script automation using <a href="https://developers.google.com/apps-script/samples">the official documentation here</a>.

By the way you can also subscribe to my blog using Monitoro. Then you can receive an alert on Telegram, Discord or any chat app. Or send my posts to Google Sheets, Airtable and trigger Zapier. More on this at <a href="https://monitoro.co">https://monitoro.co</a>.

[1] <a href="https://en.wikipedia.org/wiki/Technological_singularity">https://en.wikipedia.org/wiki/Technological_singularity</a>

[2] <a href="https://en.wikipedia.org/wiki/Not_invented_here">https://en.wikipedia.org/wiki/Not_invented_here</a>

© 2026 Omar Kamali. All rights reserved.
Made in 🇲🇦 & 🇩🇪