gmail-outreach
Outbound email actions — periodic relationship touchbase messages and batch unsubscribe from newsletters/marketing. Combines gmail-touchbase + gmail-unsubscribe into one skill.
Best use case
gmail-outreach is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Outbound email actions — periodic relationship touchbase messages and batch unsubscribe from newsletters/marketing. Combines gmail-touchbase + gmail-unsubscribe into one skill.
Teams using gmail-outreach should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/gmail-outreach/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How gmail-outreach Compares
| Feature / Agent | gmail-outreach | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Outbound email actions — periodic relationship touchbase messages and batch unsubscribe from newsletters/marketing. Combines gmail-touchbase + gmail-unsubscribe into one skill.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Gmail Outreach — Touchbase + Unsubscribe
Outbound email management: maintain relationships and remove noise. Combines two functions into one skill.
## Prerequisites
- OAuth tokens in `~/.gmail-{account}/credentials.json`
- Client credentials in `~/.gmail-mcp/oauth-env.json`
- Contact CSVs normalized (see `contact-manager` skill)
- `gmail-multi-account` skill configured
## Part 1: Touchbase — Relationship Maintenance
Identify contacts due for periodic outreach and draft check-in messages.
### Contact Cadence Rules
| Contact Category | Cadence | Trigger |
|---|---|---|
| Active clients | 30 days | Last email exchange > 30 days ago |
| GTM prospects | 21 days | No engagement in 21 days |
| Important vendors | 90 days | No interaction in 90 days |
| Alumni network | 180 days | No contact in 6 months |
| Recruiters | 90 days | No response in 90 days |
| Family/personal | 30 days | No contact in 30 days |
### Touchbase Workflow
1. Load contact CSV for the account
2. Check last interaction date (search Gmail for sent-to + received-from)
3. Filter contacts past their cadence threshold
4. Sort by priority: clients > prospects > vendors > network
5. Draft personalized check-in messages using communication style profiles (#1986)
### Draft Templates
**Client check-in:**
```
{FirstName},
Hope all is well on your end. It's been a few weeks since we last connected — just wanted to check in and see if there's anything I can help with on the [project] front.
Let me know if you'd like to catch up briefly.
Thank you,
Vamsee
```
**Touchbase with GTM prospect:**
```
{FirstName},
I wanted to follow up on our earlier conversation about [topic]. I've been working on [recent project/capability] and thought it might be relevant to what you're doing at [Company].
Would love to catch up when you have a moment.
Thank you,
Vamsee Achanta, P.E.
```
**Network check-in:**
```
Hey {FirstName},
Long time no talk. Hope [work/family/life] has been treating you well.
Let's catch up when you get a chance.
Vamsee
```
### Per-Account Style Profiles
Load the full style profile for the sending account before drafting any message:
```
config/email/ace-style.yaml — professional-technical, concise (2-5 sentences)
config/email/personal-style.yaml — casual-terse, ultra-short (1-2 sentences)
config/email/skestates-style.yaml — business-formal-warm, medium (3-6 sentences)
```
Each profile defines greeting, closing, signature, tone, formality rules, and
calibration examples. Check `formality_rules` to adjust for context (new client
vs ongoing thread, vendor follow-up vs family matters). See `config/email/README.md`
for the full usage guide. Reference: #1986.
Quick reference (see YAML files for complete rules):
| Account | Tone | Greeting | Closing | Signature |
|---|---|---|---|---|
| ace | Professional-technical | "{FirstName}," | "Thank you," | "Vamsee Achanta, P.E." (formal) / "Vamsee" (casual) |
| personal | Casual/abbreviated | "{FirstName}," or none | "Vamsee" or none | "Vamsee" |
| skestates | Business-formal-warm | "{FirstName}," | "Thank you very much," | "Vamsee" (on behalf of "the owners" / "SKEstates Inc") |
### Execution
```python
import json, urllib.request, urllib.parse, os
from datetime import datetime, timedelta
def search_gmail_for_contact(email, account, token):
"""Check for last interaction with an email address"""
query = urllib.parse.quote(f"from:{email} OR to:{email}")
result = gmail_get(f"users/me/messages?q={query}&maxResults=1", token)
if result.get("messages"):
msg = gmail_get(f"users/me/messages/{result['messages'][0]['id']}?format=metadata", token)
hdrs = {h["name"]: h["value"] for h in msg.get("payload", {}).get("headers", [])}
return hdrs.get("Date", "")
return None
def get_touchbase_candidates(account, contacts_csv, cadence_days=30):
"""Get contacts due for touchbase"""
import csv
cutoff = datetime.now() - timedelta(days=cadence_days)
token = refresh_token(account)
candidates = []
with open(contacts_csv) as f:
reader = csv.DictReader(f)
for row in reader:
email = row.get("email", "")
category = row.get("category", "")
last_contact = search_gmail_for_contact(email, account, token)
if last_contact:
contact_date = datetime.strptime(last_contact[:24], "%a, %d %b %Y %H:%M:%S")
if contact_date < cutoff:
candidates.append({
"email": email,
"name": row.get("name", ""),
"category": category,
"last_contact": last_contact,
})
return candidates
```
## Part 2: Unsubscribe — Batch Noise Removal
Identify emails with List-Unsubscribe headers and batch process unsubscribes.
### Unsubscribe Detection
```python
def find_unsubscribe_candidates(account, max_results=200):
"""Scan inbox for emails with List-Unsubscribe headers"""
token = refresh_token(account)
search = gmail_get(f"users/me/messages?maxResults={max_results}", token)
candidates = []
for msg_stab in search.get("messages", []):
detail = gmail_get(
f"users/me/messages/{msg_stab['id']}?format=metadata&metadataHeaders=List-Unsubscribe&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date&metadataHeaders=List-Unsubscribe-Post",
token
)
hdrs = {h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", [])}
if hdrs.get("List-Unsubscribe"):
method = "http" if hdrs["List-Unsubscribe"].startswith("http") else "mailto"
one_click = bool(hdrs.get("List-Unsubscribe-Post", ""))
candidates.append({
"msg_id": msg_stab["id"],
"from": hdrs.get("From", ""),
"subject": hdrs.get("Subject", ""),
"date": hdrs.get("Date", ""),
"unsubscribe_url": hdrs.get("List-Unsubscribe", ""),
"method": method,
"one_click": one_click,
})
return candidates
```
### Unsubscribe Execution
HTTP/one-click unsubscribe (POST to the URL):
```python
def execute_unsubscribe(url, method="http"):
"""Execute unsubscribe via HTTP POST or mailto"""
if method == "http" and url.startswith("http"):
req = urllib.request.Request(url, method="POST")
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return {"status": resp.status, "success": True}
except Exception as e:
return {"error": str(e), "success": False}
elif method == "mailto":
# Send empty email to unsubscribe address
mailto_addr = url.replace("<mailto:", "").replace(">", "").split(",")[0]
return {"method": "mailto", "address": mailto_addr}
return {"error": "unsupported method", "success": False}
```
### Unsubscribe + Delete Pipeline
```python
def unsubscribe_and_delete(account, domains_to_remove, max_per_domain=500, dry_run=False):
"""Unsubscribe from domains and delete matching emails"""
token = refresh_token(account)
stats = {"unsubscribed": 0, "deleted": 0, "skipped": 0}
for domain in domains_to_remove:
query = f"from:{domain} has:List-Unsubscribe"
search = gmail_get(f"users/me/messages?q={urllib.parse.quote(query)}&maxResults={max_per_domain}", token)
for msg_stab in search.get("messages", []):
detail = gmail_get(f"users/me/messages/{msg_stab['id']}?format=metadata", token)
hdrs = {h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", [])}
unsub_url = hdrs.get("List-Unsubscribe", "")
if unsub_url:
method = "http" if unsub_url.startswith("http") else "mailto"
if not dry_run:
execute_unsubscribe(unsub_url, method)
gmail_post(f"users/me/messages/{msg_stab['id']}/trash", token, {})
stats["unsubscribed"] += 1
stats["deleted"] += 1
else:
stats["skipped"] += 1
return stats
```
## Safety Rules
1. ALWAYS show unsubscribe candidates to user before executing
2. First run: execute in dry_run mode and show what would happen
3. Never unsubscribe domains marked as REVIEW in email-routing.yaml
4. Touchbase drafts: ALWAYS require user approval before sending
5. Batch unsubscribe: max 20 domains per batch to avoid Gmail rate limits
6. Log all unsubscribe actions — track what was unsubscribed and when
7. Unsubscribe list is maintained in `~/.hermes/unsubscribe-log.yaml`
## Usage Examples
### Find touchbase candidates for ace account
```bash
cd /mnt/local-analysis/workspace-hub
# Load contact CSV, check last interaction dates, produce candidate list
```
### Find unsubscribe candidates
```python
candidates = find_unsubscribe_candidates("personal", max_results=200)
# Display: sender, subject, unsubscribe method (HTTP/mailto), one-click support
```
### Execute batch unsubscribe (after user approval)
```python
domains = ["promote.weebly.com", "e.swimoutlet.com", "lists.wikimedia.org"]
stats = unsubscribe_and_delete("personal", domains, dry_run=False)
# Returns: {"unsubscribed": N, "deleted": N, "skipped": N}
```
## Unsubscribe Watchlist (from email-routing.yaml)
```
CONFIRMED DELETE (safe to unsubscribe + delete):
collide.io, skylineseven.ccsend.com, promote.weebly.com
e.swimoutlet.com, email.myflighthub.com, mail.urbanairparks.com
e.stantonoptical.com, lists.wikimedia.org, jongordon.com
atticbuddies.com, email.theparkingspot.com, marketing.goindigo.in
deeplearning.ai, gamemail.com, suzeorman.com
REVIEW FIRST (do not auto-unsubscribe):
substack.com, info.marineinsight.com, indianstarllc.ccsend.com
rigzonemail.com, m.learn.coursera.org, irctc.co.in
info.dpam.com, blueskysfund.com, cincsystems.net
```
## Deprecation Notice
This skill replaces:
- `gmail-touchbase` — functionality merged into this skill
- `gmail-unsubscribe` — functionality merged into this skill
## Pitfalls
1. one-click unsubscribe (List-Unsubscribe-Post header) requires POST to the URL with `List-Unsubscribe` header set to "1"
2. Some unsubscribe URLs are POST-only and require specific headers — test before batch
3. Touchbase cadence: use Gmail search for last interaction, not local timestamps (more reliable)
4. Don't touchbase with contacts who explicitly opted out — check "do_not_contact" column in contact CSV
5. Gmail API rate limits — batch requests with 0.15s delays between calls
6. Touchbase emails should feel personal — avoid obvious template language
7. Always cross-reference against email-routing.yaml REVIEW list before auto-unsubscribingRelated Skills
gmail-triage
Daily multi-account Gmail inbox triage — scan unread, classify by urgency, cross-reference contacts, generate actionable digest. Supports ace/personal/skestates accounts.
gmail-operations
Class-level Gmail and email operations: multi-account setup, OAuth, triage, extraction, archiving, attachments, unsubscribe, and touchbase workflows.
gmail-multi-account
Multi-account Gmail management via himalaya CLI. Three accounts (aceengineer, achantav, skestates) with distinct triage rules, contact DBs, and tone profiles. Foundation skill for email automation.
gmail-headless-oauth
Manual OAuth2 token exchange for Gmail on headless servers. Bypass gmail-mcp-multiauth browser requirement. Generate auth URLs, exchange codes, manage multi-account credentials with auto-refresh.
gmail-extract-and-act
The email-as-queue workflow — extract structured data from emails, act on it, track thread state, and delete emails when topics complete. Email is transient; extracted data is persistent.
gmail-attachment-to-document
Download attachments from Gmail threads, parse their content (Excel, PDF), extract structured data, and save to target repos with proper legal scanning.
gmail-unsubscribe
Identify and batch-unsubscribe from newsletters and marketing emails across Gmail accounts. Scans List-Unsubscribe headers, generates candidates, executes with user approval.
gmail-touchbase
Periodic relationship maintenance via email — identify contacts due for outreach, draft personalized check-ins, queue for user approval. Supports per-account tone and cadence.
gmail-extract-archive
DEPRECATED — superseded by gmail-extract-and-act. Extract Gmail data into archive repo, parse attachments, legal scan, then delete. Uses archive-everything model.
gmail-extract-and-clean
DEPRECATED — superseded by gmail-extract-and-act. Extract emails from Gmail to /mnt/ace/<repo>/, archive to repos, commit, then delete. Uses archive-everything model.
gmail-email-to-repo-extraction
Extract structured data from Gmail inbox emails, enrich with domain-specific classification, legal-scan against deny list, commit to appropriate repo, then optionally delete originals.
gmail-data-extraction
Extract structured data from Gmail emails using REST API (no pip dependencies). Covers inbox scanning, subject line regex extraction, email text parsing, thread-aware drafting, and legal-scan-before-commit workflow.