A CLI bulk email sender that uses the Google Workspace CLI (gws) to send personalized emails from your Google Workspace or Gmail account. Supports plain text and HTML email bodies with template placeholders.
- Python 3.6+
- Node.js / npm (to install the
gwsCLI) - A Google account with Gmail enabled
npm install -g @googleworkspace/cliVerify it installed:
gws --versiongit clone https://github.com/marksftw/gmassmailer.git
cd gmassmailerpython3 send_emails.py -c contacts.csv -s subject.txt -b body.txt --dry-runpython3 send_emails.py -c contacts.csv -s subject.txt -b body.txtpython3 send_emails.py -c contacts.csv -s subject.txt -b body.htmlSend a single test email to verify everything works. You'll be prompted for a recipient email address — no CSV file needed:
python3 send_emails.py -s subject.txt -b body.html --testThe subject will be prefixed with [TEST] and template placeholders will be filled with "Test User".
python3 send_emails.py -c contacts.csv -s subject.txt -b body.txt --delay 2.5| Flag | Description |
|---|---|
-c, --csv |
Path to CSV file (required unless using --test) |
-s, --subject |
Path to subject line file (required) |
-b, --body |
Path to body template file (required) |
--delay |
Seconds between sends (default: 1.0) |
--dry-run |
Preview emails without sending |
--test |
Send a single test email — prompts for a recipient address |
Before sending, validate that all email addresses in your CSV are properly formatted:
python3 validate_emails.py contacts.csvThis will catch missing @ signs, invalid domains, empty entries, and other formatting issues. If everything looks good:
All email addresses are valid.
If there are problems, it will list them:
Found 2 invalid email(s):
Row 3: John Smith - john@
Row 5: Jane Doe - (empty)
You need three files to send emails:
A CSV file with first_name, last_name, and email columns:
first_name,last_name,email
jane,doe,jane.doe@example.com
john,smith,john.smith@example.comA single line with your email subject:
Your Subject Line Here
Plain text (body.txt):
Hello {{first_name}} {{last_name}},
This is a sample email body.
Thanks,
Your Name
HTML (body.html):
<!DOCTYPE html>
<html>
<body>
<h1>Hello {{first_name}},</h1>
<p>This is an <strong>HTML email</strong>.</p>
</body>
</html>The script auto-detects the format based on file extension (.html/.htm = HTML, anything else = plain text).
Use these placeholders in your body and they'll be replaced per-contact:
| Placeholder | Replaced with |
|---|---|
{{first_name}} |
Contact's first name (title case) |
{{last_name}} |
Contact's last name (title case) |
{{email}} |
Contact's email address |
- Use
--dry-runfirst to verify your emails look correct before sending - Use
--testto send a single test email before doing a full send - HTML emails support
<img>tags with externally hosted images (e.g., Imgur, your own server) - The
emails/directory is gitignored, so you can keep your real contact lists and email templates there without committing them
Before you can send emails, you need to set up OAuth credentials in the Google Cloud Console so that gws can authenticate with your Gmail account.
- Go to Google Cloud Console
- Click Select a project > New Project
- Name your project and click Create
- In your project, search for Gmail API in the top search bar
- Click Gmail API and click Enable
- Go to APIs & Services > OAuth consent screen
- Select External (or Internal if using Google Workspace)
- Fill in the required fields (app name, support email)
- Under Scopes, add:
https://www.googleapis.com/auth/gmail.modifyopenidhttps://www.googleapis.com/auth/userinfo.email
- Under Test users, add the Gmail/Workspace email you'll send from
- Click Save
- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Application type: Desktop app (important — do not use "Web application")
- Click Create and download the JSON file
- Move it to the gws config directory:
cp ~/Downloads/client_secret_*.json ~/.config/gws/client_secret.json
gws auth loginThis will print a URL. Open it in your browser, sign in with your Google account, and approve access. The gws CLI listens on a local port to catch the callback automatically. Once authenticated, your credentials are stored locally and you're ready to send.