Sends a personalised email to each row of a Google Sheet, records every send, and never emails the same person twice.
Uses
Gmail
Google Sheets
Mail merge a personalised email to every row of a spreadsheet
Give it a spreadsheet of recipients and a message written once, with merge fields where each row's first name and company should go. Each time you run it, it sends one personalised email per row from your own Gmail account.
Nothing in your recipient tab is ever changed. A separate 'Sent log' tab is added alongside it and one line is appended there for every email sent. Every address is checked against that log before sending, so running it again skips everyone already written to and catches an address that appears twice. A row with no usable email, or a blank cell where the message needs a name or company, is reported and skipped rather than sent a half-finished greeting. If an email goes out but the log cannot be written, the run stops at once rather than carrying on blind.
Your sheet needs a header row with an 'Email' column, plus 'First Name' and 'Company' if your message uses them. Each run sends only as many emails as you allow, so your first run can be a single test.
sheetUrl to blank.Set sheetUrl to blank.sheetTabName to blank.Set sheetTabName to blank.emailSubject to blank.Set emailSubject to blank.emailBody to blank.Set emailBody to blank.sentLogTabName to Sent log.Set sentLogTabName to Sent log.maxEmailsThisRun to 1.Set maxEmailsThisRun to 1.usesFirstName to emailSubject contains {{First Name}} or emailBody contains {{First Name}}.Set usesFirstName to emailSubject contains {{First Name}} or emailBody contains {{First Name}}.usesCompany to emailSubject contains {{Company}} or emailBody contains {{Company}}.Set usesCompany to emailSubject contains {{Company}} or emailBody contains {{Company}}.sentLogIsNew to true.Set sentLogIsNew to true.createLogTabErrorIf a step below fails, save the error as createLogTabErrorsentLogTabName in sheetUrl.Create the tab sentLogTabName in sheetUrl.sentLogIsNew to false.Change sentLogIsNew to false.sentLogIsNew is yesIf sentLogIsNew is yessentLogTabName in sheetUrl.Add a row to sentLogTabName in sheetUrl.sentLogTabName in sheetUrl. Save it as sentLog.Read column 'A' of sentLogTabName in sheetUrl. Save it as sentLog.alreadySent to an empty list.Set alreadySent to an empty list.loggedAddress in sentLog.values:For each loggedAddress in sentLog.values:loggedAddressErrorIf a step below fails, save the error as loggedAddressErrorloggedAddress in lowercase to the end of alreadySent and save it as alreadySent.Add loggedAddress in lowercase to the end of alreadySent and save it as alreadySent.sentThisRun to 0.Set sentThisRun to 0.skippedRows to 0.Set skippedRows to 0.alreadySentRows to 0.Set alreadySentRows to 0.sheetUrl. Save it as recipientSheet.Read the rows of sheetUrl. Save it as recipientSheet.row in recipientSheet.rows:For each row in recipientSheet.rows:sentThisRun is greater than or equal to maxEmailsThisRunIf sentThisRun is greater than or equal to maxEmailsThisRunrowLabel to row row.row_number as text.Set rowLabel to row row.row_number as text.recipient to blank.Set recipient to blank.recipientErrorIf a step below fails, save the error as recipientErrorrow.Email lowercase and save it as recipient.Make row.Email lowercase and save it as recipient.recipient to blank.Change recipient to blank.recipient is empty or recipient doesn't contain @If recipient is empty or recipient doesn't contain @rowLabel: the Email column is empty or does not hold an email address.Show in the output: Skipped rowLabel: the Email column is empty or does not hold an email address.skippedRows.Add 1 to skippedRows.alreadySent contains recipientIf alreadySent contains recipientrowLabel: recipient is already listed in the '@sentLogTabName' tab.Show in the output: Skipped rowLabel: recipient is already listed in the '@sentLogTabName' tab.alreadySentRows.Add 1 to alreadySentRows.firstName to blank.Set firstName to blank.firstNameErrorIf a step below fails, save the error as firstNameErrorrow["First Name"] and save it as firstName.Trim row["First Name"] and save it as firstName.firstName to blank.Change firstName to blank.company to blank.Set company to blank.companyErrorIf a step below fails, save the error as companyErrorrow.Company and save it as company.Trim row.Company and save it as company.company to blank.Change company to blank.usesFirstName is yes and firstName is emptyIf usesFirstName is yes and firstName is emptyrowLabel: your message uses {{First Name}} but that cell is empty.Show in the output: Skipped rowLabel: your message uses {{First Name}} but that cell is empty.skippedRows.Add 1 to skippedRows.usesCompany is yes and company is emptyIf usesCompany is yes and company is emptyrowLabel: your message uses {{Company}} but that cell is empty.Show in the output: Skipped rowLabel: your message uses {{Company}} but that cell is empty.skippedRows.Add 1 to skippedRows.{{Email}} with recipient in emailSubject with {{First Name}} replaced by firstName with {{Company}} replaced by company and save it as mergedSubject.Replace {{Email}} with recipient in emailSubject with {{First Name}} replaced by firstName with {{Company}} replaced by company and save it as mergedSubject.{{Email}} with recipient in emailBody with {{First Name}} replaced by firstName with {{Company}} replaced by company and save it as mergedBody.Replace {{Email}} with recipient in emailBody with {{First Name}} replaced by firstName with {{Company}} replaced by company and save it as mergedBody.emailSent to true.Set emailSent to true.sendErrorIf a step below fails, save the error as sendErrorrecipient.Send an email to recipient.
Subject: mergedSubjectmergedBodyemailSent to false.Change emailSent to false.recipient: sendError as text.Show in the output: Could not send to recipient: sendError as text.emailSent is noIf emailSent is noskippedRows.Add 1 to skippedRows.logWriteErrorIf a step below fails, save the error as logWriteErrorsentLogTabName in sheetUrl.Add a row to sentLogTabName in sheetUrl.recipient but could not add it to the '@sentLogTabName' tab. The run stopped here so that nobody is emailed twice next time - fix the spreadsheet, then run it again.Stop and report a problem: Sent the email to recipient but could not add it to the '@sentLogTabName' tab. The run stopped here so that nobody is emailed twice next time - fix the spreadsheet, then run it again.recipient to the end of alreadySent and save it as alreadySent.Add recipient to the end of alreadySent and save it as alreadySent.sentThisRun.Add 1 to sentThisRun.sentThisRun as text email(s) this run. alreadySentRows as text row(s) had already been emailed and skippedRows as text row(s) were skipped. Raise 'maxEmailsThisRun' and run it again to carry on down the list.Show in the output: Sent sentThisRun as text email(s) this run. alreadySentRows as text row(s) had already been emailed and skippedRows as text row(s) were skipped. Raise 'maxEmailsThisRun' and run it again to carry on down the list.