DEV Community

Abdulrasaq Agboluaje
Abdulrasaq Agboluaje

Posted on

Sending email as your users: why I ditched Gmail OAuth for App Passwords

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())
Enter fullscreen mode Exit fullscreen mode

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
unreachable
. The cause: the container resolved smtp.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
Enter fullscreen mode Exit fullscreen mode

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)