● public · updated 2 days ago · id sgkbrkxaxs ·5,071 chars

How to send emails using Gmail programmatically (Python + app password)

● How to send emails using Gmail programmatically (Python + app password).md 5,071 chars · read-only
# How to send emails using Gmail programmatically

A minimal way to send emails from a Gmail account in code — no SendGrid, no Postmark, no third-party libs. Just SMTP over a Gmail App Password.

This works for any script, agent, or backend that needs to send email from an existing Gmail address.

---

## What you need

1. A Gmail account with **2-Step Verification enabled**.
2. A **Gmail App Password** — a 16-character token Google issues specifically for SMTP/IMAP access.
3. Python 3.10+ (stdlib only — `smtplib`, `csv`, `email.message`).

---

## Step 1 — Generate a Gmail App Password

Go to: https://myaccount.google.com/apppasswords

If the page says app passwords aren't available, 2FA isn't on. Enable 2-Step Verification at https://myaccount.google.com/security first.

Generate a new password. Google shows the 16-character string **once** — copy it immediately. Format looks like `abcd efgh ijkl mnop`. The spaces are optional; Gmail accepts it either way.

Treat this like a password — don't commit it, don't paste it in chat. Rotate if exposed.

---

## Step 2 — Connect via SMTP

Gmail's SMTP endpoint:

| Setting | Value |
|---|---|
| Host | `smtp.gmail.com` |
| Port | `587` (STARTTLS) or `465` (SSL) |
| Username | your full Gmail address |
| Password | the 16-char app password |

Minimal Python:

```python
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "you@gmail.com"
msg["To"]   = "recipient@example.com"
msg["Subject"] = "hello"
msg.set_content("Body goes here.")

with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
    smtp.starttls()
    smtp.login("you@gmail.com", "abcd efgh ijkl mnop")
    smtp.send_message(msg)
```

That's the whole protocol. Everything below is wrapping that in a workflow.

---

## Step 3 — Pattern for sending personalized emails to a list

Three files: env, recipients, template. The script reads all three, previews, asks for confirmation, then sends one-by-one with a delay.

### `.env`
```dotenv
GMAIL_USER_1=you@gmail.com
GMAIL_APP_PASSWORD_1=xxxx xxxx xxxx xxxx
```

Supports multiple accounts (`_2`, `_3`, …) so the script can prompt which one to send from.

### `recipients.csv`
```csv
name,email
Alice,alice@example.com
Bob,bob@example.com
```

### `template.txt`
First line is the subject. `{name}` is substituted per recipient.
```
Subject: <subject line>

Hey {name},

<body>
```

### `send_emails.py`

Key parts:
- **Load env** — parse `.env` manually, no python-dotenv needed.
- **Discover accounts** — regex over env vars matching `GMAIL_USER_N`, pair with the matching password.
- **Validate emails** — regex filter, dedupe by lowercase address.
- **Render** — naive `{key}` replacement, no template engine.
- **Dry-run flag** — print every rendered email without opening SMTP.
- **Confirmation prompt** — list all recipients, require `y` before live send.
- **Delay between sends** — `time.sleep(4)`. Gmail rate-limits bursts.

The whole thing is ~120 lines of stdlib Python. No dependencies.

---

## Run it

```bash
python3 send_emails.py --dry-run --limit 3   # preview, no send
python3 send_emails.py --delay 5             # live send
```

Always dry-run first. It catches template errors (`{name}` typos, malformed CSV rows) before you mail real people.

---

## Why these design choices matter

**One-by-one, not BCC.** BCC blasts look like spam to filters and to recipients (when their client says "Bcc:"). Sending individually means each recipient sees only their own address — looks personal, deliverability stays high.

**Delays between sends.** Gmail's free tier flags rapid sends as suspicious activity. 4–5 seconds is the sweet spot for outreach volume. Faster and you'll start hitting `421 4.7.0` rate-limit responses.

**Dry-run mode.** Templates with `{missing_field}` will silently send the literal `{missing_field}` to recipients. Dry-run renders every email to stdout so you catch it.

**Confirmation prompt.** Treating "send 50 emails" the same as "delete a folder" — make the human eyeball the list and type `y`. Cheap insurance.

---

## Limits and gotchas

- **Free Gmail:** ~500 sends/day. Workspace: 2,000/day.
- **App password rejected:** verify 2FA is on. Without it, app passwords cannot exist.
- **Port 587 blocked on some networks** (corporate Wi-Fi, hotels). Switch to port 465 with `smtplib.SMTP_SSL`.
- **Landing in spam:** real reply-to, recognizable From name, no tracking links on first contact, plain-text not HTML for outreach. Warmer addresses (used regularly) deliver better than cold ones.
- **Don't commit `.env`.** Add it to `.gitignore` immediately. App passwords leaked in public repos are auto-revoked by Google's scanners — but only after some unknown lag.

---

## When this is the right tool

- < 500 emails/day, personalized, ad-hoc.
- One-off scripts, internal tools, agent workflows.
- Anywhere setting up Postmark / Resend / SendGrid is overkill.

**Switch to a real ESP** for transactional email at scale (>1k/day), bounce handling, deliverability monitoring, or anything customer-facing in production.
feed this URL to your agent
https://npad.run/p/how-to-send-emails-using-gmail-programmatically-sgkbrkxaxs
install npad

paste the URL into Claude Code, Codex or Cursor — the agent fetches the full body via npad's API.