Send email from an existing Gmail mailbox
Send a controlled message through Gmail's API using scoped OAuth and base64url MIME, while keeping mailbox sending separate from application delivery.
Reviewed
Use the Gmail API when a message should be sent from an existing user’s authorized Gmail mailbox. The API accepts a MIME message encoded as base64url in its raw field. It is not an arbitrary-domain delivery API, and Gmail’s account policies and sending limits still apply.
Prepare the authorized connection
Complete Gmail authorization and save the shared gmail-client.mjs module. The grant must include gmail.send or another deliberately selected scope that authorizes sending. Read-only mailbox access does not grant permission to send.
Set GMAIL_FROM to the connected mailbox’s approved sender address, and GMAIL_TEST_TO to a recipient you control. An alternate From identity must be configured and allowed by Gmail; do not assume the API can impersonate any address in your organization.
For a test, use plain ASCII bare email addresses and a fixed subject/body. The example below deliberately avoids user-provided header values, display names, attachments, and complex MIME construction.
Build and send one message
Save beside the shared client as send-gmail.mjs:
import { gmail } from "./gmail-client.mjs";
const from = process.env.GMAIL_FROM;
const to = process.env.GMAIL_TEST_TO;
const simpleAddress = /^[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@[A-Za-z0-9.-]+$/;
if (!from || !to || !simpleAddress.test(from) || !simpleAddress.test(to)) {
throw new Error("Set approved bare ASCII GMAIL_FROM and GMAIL_TEST_TO addresses");
}
const mime = [
`From: ${from}`,
`To: ${to}`,
`Date: ${new Date().toUTCString()}`,
"Subject: Gmail application integration test",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=UTF-8",
"Content-Transfer-Encoding: 7bit",
"",
"A controlled message from the connected Gmail mailbox.",
"",
].join("\r\n");
const result = await gmail.users.messages.send({
userId: "me",
requestBody: { raw: Buffer.from(mime, "utf8").toString("base64url") },
}, { timeout: 15000, retry: false });
console.log({ messageId: result.data.id, threadId: result.data.threadId });
node send-gmail.mjs sends real email. Run it only after the recipient and copy are approved. This is a bounded MIME example, not a complete address validator or a production message composer. For arbitrary user content, encoded headers, HTML, or attachments, use a maintained MIME library and validate inputs rather than extending string concatenation.
Check mailbox state and delivery evidence
A returned message ID indicates that the API accepted the operation. Verify the message in the connected mailbox’s Sent folder and check the controlled recipient. The Gmail message ID and thread ID are provider-specific identifiers; neither is a MailKite event ID or a Pub/Sub notification ID.
Record the application’s intended send before calling Gmail. A network timeout may occur after acceptance, so treat it as an unknown result rather than automatically issuing a second send. Investigate mailbox state and available evidence before an operator retries it.
For replies, preserve the correct In-Reply-To, References, subject, and Gmail thread association according to Google’s threading requirements. Setting a random threadId does not create a valid reply relationship.
Troubleshoot without broadening access
- Insufficient permission: reconnect with the required scope and user/admin approval; do not request full mailbox access just to fix sending.
- Invalid grant: resolve token revocation, account policy, or client mismatch through the authorization flow.
- Rate or account limits: queue or stop the workflow; do not rotate mailboxes to evade enforcement.
- Wrong sender: check the authenticated mailbox and its allowed send-as identity.
When MailKite is the better boundary
For independent transactional messages, use MailKite’s sending SDK from a verified application domain instead of tying product delivery to a personal mailbox. That does not send as the user’s Gmail mailbox or automatically create a Gmail Sent-folder copy. Keep the two transports explicit and never submit the same message to both during a cutover.
References