Sending Email via the Gmail API from a VPS
A guide for using Google Workspace to send mail over HTTPS (port 443), bypassing blocked SMTP ports entirely.
How it works
Instead of connecting to smtp.gmail.com on ports 25/465/587, your server makes authenticated HTTPS requests to https://gmail.googleapis.com. Since port 443 is never blocked, this works on any VPS.
There are two authentication approaches:
| Approach | Best for | Notes |
|---|---|---|
| Service account + domain-wide delegation | Servers, cron jobs, apps (headless) | Recommended for VPS use. No browser needed, no token refresh headaches. Requires Workspace admin access. |
| OAuth 2.0 user consent | Desktop apps, scripts run by a human | Requires a one-time browser login and storing a refresh token. |
This guide focuses on the service account approach since it suits unattended server use, with the OAuth flow covered briefly at the end.
Part 1: Google Cloud Console setup
1.1 Create a project
- Go to console.cloud.google.com
- Click the project dropdown (top left) then New Project
- Name it something like
vps-mailerand create it
1.2 Enable the Gmail API
- With your project selected, go to APIs & Services > Library
- Search for Gmail API
- Click it, then click Enable
1.3 Create a service account
- Go to APIs & Services > Credentials
- Click Create Credentials > Service account
- Name it (e.g.
mailer), click Create and Continue - Skip the optional role/access steps, click Done
- Click the new service account, go to the Keys tab
- Add Key > Create new key > JSON, then download the file
Keep this JSON file secret. It is effectively a password.
1.4 Note the client ID
On the service account's Details tab, copy the Unique ID (a long number). You need it for the next step.
Part 2: Google Workspace Admin setup
Domain-wide delegation lets the service account send mail as a real user in your domain (e.g. noreply@yourdomain.com).
- Go to admin.google.com as a super admin
- Navigate to Security > Access and data control > API controls
- Click Manage Domain Wide Delegation
- Click Add new
- Paste the service account's Client ID (the unique ID from step 1.4)
- In OAuth scopes, enter:
https://www.googleapis.com/auth/gmail.send - Click Authorize
Use only the gmail.send scope unless you also need to read mail. Narrow scopes limit damage if the key leaks.
The user you impersonate (e.g. noreply@yourdomain.com) must be a real Workspace user (or an alias of one).
Part 3: Sending mail from your VPS (Python)
3.1 Install dependencies
pip install google-auth google-api-python-client
3.2 Upload your key
Copy the JSON key to your VPS and lock down permissions:
chmod 600 /etc/vps-mailer/service-account.json
3.3 Send a message
import base64
from email.message import EmailMessage
from google.oauth2 import service_account
from googleapiclient.discovery import build
KEY_FILE = "/etc/vps-mailer/service-account.json"
SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
SENDER = "noreply@yourdomain.com" # the Workspace user to send as
def get_service():
creds = service_account.Credentials.from_service_account_file(
KEY_FILE, scopes=SCOPES
)
# Impersonate the Workspace user
delegated = creds.with_subject(SENDER)
return build("gmail", "v1", credentials=delegated)
def send_email(to: str, subject: str, body: str, html: str | None = None):
msg = EmailMessage()
msg["To"] = to
msg["From"] = SENDER
msg["Subject"] = subject
msg.set_content(body)
if html:
msg.add_alternative(html, subtype="html")
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
service = get_service()
result = service.users().messages().send(
userId="me", body={"raw": raw}
).execute()
print(f"Sent, message id: {result['id']}")
if __name__ == "__main__":
send_email(
to="someone@example.com",
subject="Test from my VPS",
body="Hello! This was sent over HTTPS via the Gmail API.",
)
Run it:
python send_test.py
3.4 Attachments
def send_with_attachment(to, subject, body, filepath):
msg = EmailMessage()
msg["To"] = to
msg["From"] = SENDER
msg["Subject"] = subject
msg.set_content(body)
with open(filepath, "rb") as f:
data = f.read()
import mimetypes
ctype, _ = mimetypes.guess_type(filepath)
maintype, subtype = (ctype or "application/octet-stream").split("/", 1)
msg.add_attachment(
data, maintype=maintype, subtype=subtype,
filename=filepath.split("/")[-1]
)
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
get_service().users().messages().send(
userId="me", body={"raw": raw}
).execute()
Part 4: Node.js equivalent
npm install googleapis
const { google } = require("googleapis");
const SENDER = "noreply@yourdomain.com";
async function sendEmail(to, subject, body) {
const auth = new google.auth.GoogleAuth({
keyFile: "/etc/vps-mailer/service-account.json",
scopes: ["https://www.googleapis.com/auth/gmail.send"],
clientOptions: { subject: SENDER }, // impersonation
});
const gmail = google.gmail({ version: "v1", auth });
const message = [
`To: ${to}`,
`From: ${SENDER}`,
`Subject: ${subject}`,
`Content-Type: text/plain; charset=utf-8`,
"",
body,
].join("\r\n");
const raw = Buffer.from(message)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const res = await gmail.users.messages.send({
userId: "me",
requestBody: { raw },
});
console.log("Sent:", res.data.id);
}
sendEmail("someone@example.com", "Test", "Hello from Node!");
Part 5: DNS and deliverability
Even though Google's servers do the actual delivery, make sure your domain's DNS is correct so mail lands in inboxes:
SPF (TXT record on your root domain):
v=spf1 include:_spf.google.com ~all
DKIM: In the Workspace Admin console, go to Apps > Google Workspace > Gmail > Authenticate email, generate the DKIM key, and add the TXT record it gives you at google._domainkey.yourdomain.com. Then click Start authentication.
DMARC (TXT record at _dmarc.yourdomain.com), starting relaxed:
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
Part 6: Limits and gotchas
- Sending limits: Google Workspace caps at 2,000 recipients per day per user (500 on trial accounts). The Gmail API also counts against per-user quota units, but normal transactional volume will not hit them.
- Not for bulk mail: For newsletters or high volume, use a transactional service (SES, Postmark, Resend) instead. Google will throttle or suspend accounts used for bulk sending.
- The impersonated user must exist: You cannot send as an arbitrary address, only as a real Workspace user or one of their configured aliases.
- Key security: Treat the JSON key like a root password. Restrict file permissions, never commit it to git, and rotate it if you suspect exposure. Consider limiting delegation to only the
gmail.sendscope as shown. - Clock skew: JWT auth fails if your VPS clock is badly off. Keep NTP running (
timedatectl set-ntp true).
Appendix: OAuth flow instead of a service account
If you do not have Workspace admin access, or you are on a personal Gmail account, use OAuth:
- In Cloud Console, go to APIs & Services > OAuth consent screen, configure it (Internal for Workspace, or External + test users for personal Gmail)
- Create credentials: Create Credentials > OAuth client ID > Desktop app
- Download the client secret JSON
- Run this once on a machine with a browser:
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_secrets_file(
"client_secret.json",
scopes=["https://www.googleapis.com/auth/gmail.send"],
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as f:
f.write(creds.to_json())
- Copy
token.jsonto your VPS and load it:
from google.oauth2.credentials import Credentials
creds = Credentials.from_authorized_user_file(
"token.json",
scopes=["https://www.googleapis.com/auth/gmail.send"],
)
The refresh token keeps working indefinitely as long as the app is in "In production" status (test-mode tokens expire after 7 days) and the user does not revoke access.
