Send personalized emails from a sheet in Gmail
Originally written for bulldo.gs — republished here with the canonical link pointing home. I have a spreadsheet of names and email addresses and I want to send each person a personalized message from my Gmail account without copy-pasting or using a paid tool. // Mail merge: Sheet cols A=Name, B=Email, C=Sent // Run from Apps Script; authorize Gmail + Sheets scopes function sendMerge () { var sheet = SpreadsheetApp . getActiveSheet (); var rows = sheet . getDataRange (). getValues (); var quota = MailApp . getRemainingDailyQuota (); var sent = 0 ; for ( var i = 1 ; i < rows . length ; i ++ ) { if ( rows [ i ][ 2 ] === ' Sent ' ) continue ; if ( sent >= quota ) { Logger . log ( ' Quota reached at row ' + ( i + 1 )); break ; } var name = rows [ i ][ 0 ]; var email = rows [ i ][ 1 ]; var subject = ' Hey ' + name + ' , here is your update ' ; var body = ' Hi ' + name + ' , \n\n Your personalized content goes here. \n\n Thanks ' ; MailApp . sendEmail ( email , subject , body ); sheet . getRange ( i + 1 , 3 ). setValue ( ' Sent ' ); sent ++ ; } } Set up your sheet and open the script editor Put names in column A, email addresses in column B, and leave column C blank — the script writes 'Sent' there as it goes. Header row in row 1 is assumed; the loop starts at index 1 (row 2) to skip it. Open the script editor from Extensions > Apps Script, paste the function, and save. The first time you run sendMerge() Google will ask you to authorize two scopes: Sheets (read/write the active spreadsheet) and Gmail (send mail on your behalf). Both are required. If you only see a Sheets prompt, delete the file and re-paste — a cached partial authorization sometimes skips the Gmail scope on older script files. Why the Sent column is the whole point Consumer Google accounts cap at roughly 100 outgoing recipients per 24-hour rolling window via MailApp. If your list has 200 rows and you run the script at 11 pm, it will send 100 and log 'Quota reached at row 101'. Without the Sent check, a sec