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

  1. Go to console.cloud.google.com
  2. Click the project dropdown (top left) then New Project
  3. Name it something like vps-mailer and create it

1.2 Enable the Gmail API

  1. With your project selected, go to APIs & Services > Library
  2. Search for Gmail API
  3. Click it, then click Enable

1.3 Create a service account

  1. Go to APIs & Services > Credentials
  2. Click Create Credentials > Service account
  3. Name it (e.g. mailer), click Create and Continue
  4. Skip the optional role/access steps, click Done
  5. Click the new service account, go to the Keys tab
  6. 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).

  1. Go to admin.google.com as a super admin
  2. Navigate to Security > Access and data control > API controls
  3. Click Manage Domain Wide Delegation
  4. Click Add new
  5. Paste the service account's Client ID (the unique ID from step 1.4)
  6. In OAuth scopes, enter:
    https://www.googleapis.com/auth/gmail.send
  7. 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.send scope 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:

  1. In Cloud Console, go to APIs & Services > OAuth consent screen, configure it (Internal for Workspace, or External + test users for personal Gmail)
  2. Create credentials: Create Credentials > OAuth client ID > Desktop app
  3. Download the client secret JSON
  4. 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())
  1. Copy token.json to 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.

Was this answer helpful? 0 Users Found This Useful (0 Votes)