I'm building RecruiterReach, a tool that helps job
seekers email the recruiter behind a job posting — sent from the user's own
Gmail, so replies come back to them. That "send as the user" requirement turned
into the most annoying technical decision of the whole project. Here's what I
learned, in case it saves you the same detour.
The obvious path: Gmail API + OAuth
The "correct" way to send email on a user's behalf is the Gmail API with the
gmail.send OAuth scope. I built it, it worked, and then I hit the wall:
gmail.send is a restricted scope. To use it in production beyond ~100
users, Google requires:
- App verification (fine)
- A demo video showing the consent screen + the scope in use (tedious)
- An annual third-party CASA security assessment — which costs $500+/year
For a bootstrapped side project, a recurring $500 bill just to send email was a
non-starter. There's also a 7-day refresh-token expiry while your app is in
"testing", which means users would have to reconnect Gmail every week. Brutal UX.
The pivot: Gmail App Passwords over SMTP
Google App Passwords (with 2FA enabled) let you authenticate to Gmail's SMTP
server directly — no OAuth, no verification, no CASA. The user generates a
16-character password, you send via smtp.gmail.com. Trade-off: a little more
onboarding friction (the user has to create the app password), but $0 cost and
no Google review.
import smtplib
from email.mime.text import MIMEText
def send(sender, app_password, to, subject, body):
msg = MIMEText(body)
msg["From"], msg["To"], msg["Subject"] = sender, to, subject
with smtplib.SMTP_SSL("smtp.gmail.com", 465, timeout=30) as server:
server.login(sender, app_password)
server.sendmail(sender, [to], msg.as_string())
Store the app password encrypted at rest (I used Fernet), never plaintext.
The gotcha: [Errno 101] Network is unreachable on Render
First deploy to Render, every send failed with [Errno 101] Network is. The cause: the container resolved
unreachablesmtp.gmail.com to an IPv6
address, but the egress had no IPv6 route — and Python didn't fall back to IPv4.
The fix: force IPv4 resolution for the SMTP connection while keeping the hostname
for TLS/SNI validation.
import socket, smtplib
def gmail_smtp_login(email, app_pw, timeout=20):
orig = socket.getaddrinfo
def ipv4_only(host, port, family=0, type=0, proto=0, flags=0):
return orig(host, port, socket.AF_INET, type, proto, flags)
socket.getaddrinfo = ipv4_only
try:
s = smtplib.SMTP_SSL("smtp.gmail.com", 465, timeout=timeout)
s.login(email, app_pw)
return s
finally:
socket.getaddrinfo = orig
Note: free Render instances also block outbound SMTP ports entirely — you need
a paid instance for SMTP to work at all. That part is not fixable in code.
Would I do it again?
For a funded company that needs frictionless onboarding at scale, OAuth +
verification is worth it. For a bootstrapper who needs to ship without a $500/yr
line item, App Passwords are a totally reasonable trade — you swap a bit of
onboarding friction for zero cost and zero Google review.
If you're curious what I'm building it for, it's RecruiterReach
— finds the recruiter behind a job posting and drafts the outreach email.
Happy to answer questions about the Gmail/SMTP side in the comments. 👋
Top comments (0)