Skip to content

Instantly share code, notes, and snippets.

@woloski
Last active July 21, 2025 21:01
Show Gist options
  • Select an option

  • Save woloski/8ed001947682955b5fc9f29a4ef2ebd8 to your computer and use it in GitHub Desktop.

Select an option

Save woloski/8ed001947682955b5fc9f29a4ef2ebd8 to your computer and use it in GitHub Desktop.
Send email with gmail using service accounts

Gmail Sender

Una lib simple para enviar emails usando la API de Gmail con autenticación de Service Account.

Características

  • ✅ Envío de emails usando Gmail API
  • ✅ Soporte para Domain-Wide Delegation
  • ✅ Manejo robusto de errores
  • ✅ Validación de formatos de email
  • ✅ Código modular y reutilizable
  • ✅ Soporte para diferentes formatos de clave privada

Configuración

1. Variables de Entorno

Crea un archivo .env.local en la raíz del proyecto:

GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nTU_CLAVE_PRIVADA_AQUI\n-----END PRIVATE KEY-----"
GOOGLE_CLIENT_EMAIL="[email protected]"

2. Configuración de Google Cloud

  1. Crear un proyecto en Google Cloud Console
  2. Habilitar Gmail API
  3. Crear un Service Account
  4. Descargar las credenciales JSON
  5. Configurar Domain-Wide Delegation (si es necesario)

3. Formato de la Clave Privada

La clave privada debe estar en formato PEM. Si tienes problemas con el formato, puedes usar:

GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC+L5oU+TdRN46E\n...\n-----END PRIVATE KEY-----"

Uso

API Endpoint

POST /api/send-email
Content-Type: application/json

{
  "from": "[email protected]",
  "to": "[email protected]", 
  "subject": "Asunto del email",
  "body": "Contenido del email"
}

Respuesta

{
  "success": true,
  "messageId": "18c1234567890abc",
  "message": "Email enviado exitosamente"
}

Códigos de Error

  • MISSING_FIELDS: Campos requeridos faltantes
  • INVALID_EMAIL_FORMAT: Formato de email inválido
  • INVALID_CREDENTIALS: Credenciales inválidas
  • ACCESS_DENIED: Acceso denegado
  • UNAUTHORIZED_CLIENT: Cliente no autorizado
  • INVALID_PRIVATE_KEY: Error en formato de clave privada
  • UNSUPPORTED_KEY_FORMAT: Formato de clave no soportado

Estructura del Proyecto

├── app/
│   └── api/
│       └── send-email/
│           └── route.ts          # API endpoint
├── lib/
│   └── gmail-client.ts          # Cliente de Gmail (todo en un archivo)
└── components/                  # Componentes de UI

Funciones Disponibles

sendEmail(emailData: EmailData): Promise<EmailResponse>

Función principal para enviar emails.

import { sendEmail } from "@/lib/gmail-client"

const result = await sendEmail({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Asunto",
  body: "Contenido"
})

createGmailClient(userEmail: string)

Crea un cliente de Gmail autenticado.

cleanPrivateKey(privateKey: string): string

Limpia y formatea la clave privada.

parseFromField(from: string): FromField

Parsea el campo "from" para extraer email y nombre.

configureGmail(credentials: { privateKey: string; clientEmail: string })

Configura las credenciales de Gmail manualmente.

isConfigured(): boolean

Verifica si la configuración está completa.

Troubleshooting

Error: "DECODER routines::unsupported"

Causa: Formato incorrecto de la clave privada.

Solución:

  1. Verifica que la clave esté en formato PEM
  2. Asegúrate de que los \n estén correctamente escapados
  3. Regenera las credenciales si es necesario

Error: "invalid_grant"

Causa: Credenciales inválidas o Domain-Wide Delegation no configurado.

Solución:

  1. Verifica que el Service Account tenga los permisos correctos
  2. Configura Domain-Wide Delegation si es necesario
  3. Verifica que el email del remitente esté autorizado

Error: "access_denied"

Causa: El Service Account no tiene permisos para enviar emails.

Solución:

  1. Verifica que el Service Account tenga el scope https://www.googleapis.com/auth/gmail.send
  2. Configura Domain-Wide Delegation correctamente
  3. Verifica que el email del remitente esté en el dominio autorizado

Desarrollo

# Instalar dependencias
npm install

# Ejecutar en desarrollo
npm run dev

# Construir para producción
npm run build

Uso como Librería Independiente

Para usar este módulo en otro proyecto, simplemente copia el archivo lib/gmail-client.ts y configúralo:

Opción 1: Variables de Entorno

// .env
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nTU_CLAVE_AQUI\n-----END PRIVATE KEY-----"
GOOGLE_CLIENT_EMAIL="[email protected]"

// Uso
import { sendEmail } from "./gmail-client"

Opción 2: Configuración Manual

import { sendEmail, configureGmail } from "./gmail-client"

// Configurar manualmente
configureGmail({
  privateKey: "-----BEGIN PRIVATE KEY-----\nTU_CLAVE_AQUI\n-----END PRIVATE KEY-----",
  clientEmail: "[email protected]"
})

// Usar
const result = await sendEmail({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Asunto",
  body: "Contenido"
})

Dependencias

  • next: Framework de React
  • googleapis: Cliente oficial de Google APIs
  • @types/node: Tipos de TypeScript para Node.js
import { google } from "googleapis"
const SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
export interface EmailData {
from: string
to: string
subject: string
body: string
}
export interface EmailResponse {
success: boolean
messageId?: string
message: string
error?: string
}
export interface FromField {
email: string
name?: string
}
/**
* Configuración de variables de entorno para Gmail
*/
export const config = {
gmail: {
privateKey: process.env.GOOGLE_PRIVATE_KEY,
clientEmail: process.env.GOOGLE_CLIENT_EMAIL,
}
}
/**
* Valida que todas las variables de entorno requeridas estén configuradas
*/
export function validateConfig() {
const missingVars = []
if (!config.gmail.privateKey) {
missingVars.push("GOOGLE_PRIVATE_KEY")
}
if (!config.gmail.clientEmail) {
missingVars.push("GOOGLE_CLIENT_EMAIL")
}
if (missingVars.length > 0) {
throw new Error(`Variables de entorno faltantes: ${missingVars.join(", ")}`)
}
}
/**
* Limpia y formatea la clave privada de Google
*/
export function cleanPrivateKey(privateKey: string): string {
let cleanKey = privateKey.trim()
// Remover comillas dobles extra del inicio y final si existen
if (cleanKey.startsWith('""') && cleanKey.endsWith('""')) {
cleanKey = cleanKey.slice(2, -2)
} else if (cleanKey.startsWith('"') && cleanKey.endsWith('"')) {
cleanKey = cleanKey.slice(1, -1)
}
// Reemplazar \n literales con saltos de línea reales
if (cleanKey.includes("\\n")) {
cleanKey = cleanKey.replace(/\\n/g, "\n")
}
// Si la clave no tiene el formato PEM correcto, intentar formatearla
if (!cleanKey.includes("-----BEGIN PRIVATE KEY-----")) {
// Si es una clave en formato base64 sin formato PEM, agregar los headers
if (cleanKey.length > 100 && !cleanKey.includes("-----")) {
const chunks = []
for (let i = 0; i < cleanKey.length; i += 64) {
chunks.push(cleanKey.slice(i, i + 64))
}
cleanKey = `-----BEGIN PRIVATE KEY-----\n${chunks.join("\n")}\n-----END PRIVATE KEY-----`
}
}
// Asegurar que termina con un salto de línea
if (!cleanKey.endsWith("\n")) {
cleanKey += "\n"
}
return cleanKey
}
/**
* Parsea el campo "from" para extraer email y nombre
*/
export function parseFromField(from: string): FromField {
const nameEmailRegex = /^(.+?)\s*<(.+?)>$/
const match = from.match(nameEmailRegex)
if (match) {
return {
name: match[1].trim(),
email: match[2].trim(),
}
}
return { email: from.trim() }
}
/**
* Valida el formato de email
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
/**
* Crea el cliente de Gmail autenticado
*/
export async function createGmailClient(userEmail: string) {
validateConfig()
const privateKey = cleanPrivateKey(config.gmail.privateKey!)
const jwtClient = new google.auth.JWT({
email: config.gmail.clientEmail!,
key: privateKey,
scopes: SCOPES,
subject: userEmail,
})
await jwtClient.authorize()
return google.gmail({ version: "v1", auth: jwtClient })
}
/**
* Crea el mensaje de email en formato base64
*/
export function createEmailMessage(from: string, to: string, subject: string, body: string): string {
const parsedFrom = parseFromField(from)
const htmlBody = body.replace(/\n/g, "<br>")
const fromHeader = parsedFrom.name ? `${parsedFrom.name} <${parsedFrom.email}>` : parsedFrom.email
const emailLines = [
`From: ${fromHeader}`,
`To: ${to}`,
`Subject: ${subject}`,
"MIME-Version: 1.0",
"Content-Type: text/html; charset=utf-8",
"",
htmlBody,
]
const email = emailLines.join("\r\n")
return Buffer.from(email).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
}
/**
* Envía un email usando Gmail API
*/
export async function sendEmail(emailData: EmailData): Promise<EmailResponse> {
try {
const { from, to, subject, body } = emailData
// Validaciones básicas
if (!from || !to || !subject || !body) {
return {
success: false,
message: "Todos los campos son requeridos",
error: "MISSING_FIELDS"
}
}
const parsedFrom = parseFromField(from)
if (!isValidEmail(parsedFrom.email) || !isValidEmail(to)) {
return {
success: false,
message: "Formato de email inválido",
error: "INVALID_EMAIL_FORMAT"
}
}
const gmail = await createGmailClient(parsedFrom.email)
const encodedMessage = createEmailMessage(from, to, subject, body)
const result = await gmail.users.messages.send({
userId: "me",
requestBody: { raw: encodedMessage },
})
return {
success: true,
messageId: result.data.id || undefined,
message: "Email enviado exitosamente"
}
} catch (error: any) {
console.error("Error sending email:", error)
let errorMessage = "Error interno del servidor"
let errorCode = "INTERNAL_ERROR"
if (error.message?.includes("invalid_grant")) {
errorMessage = "Credenciales inválidas o domain delegation no configurado"
errorCode = "INVALID_CREDENTIALS"
} else if (error.message?.includes("access_denied")) {
errorMessage = "Acceso denegado. Verifica la configuración de domain-wide delegation"
errorCode = "ACCESS_DENIED"
} else if (error.message?.includes("unauthorized_client")) {
errorMessage = "Cliente no autorizado. Verifica los permisos del service account"
errorCode = "UNAUTHORIZED_CLIENT"
} else if (error.message?.includes("DECODER routines") || error.message?.includes("unsupported")) {
errorMessage = "Error en el formato de la clave privada. Verifica que GOOGLE_PRIVATE_KEY esté correctamente configurada"
errorCode = "INVALID_PRIVATE_KEY"
} else if (error.message?.includes("ERR_OSSL_UNSUPPORTED")) {
errorMessage = "Formato de clave privada no soportado. Asegúrate de que la clave esté en formato PEM correcto"
errorCode = "UNSUPPORTED_KEY_FORMAT"
}
return {
success: false,
message: errorMessage,
error: errorCode
}
}
}
/**
* Configura las credenciales de Gmail
*/
export function configureGmail(credentials: { privateKey: string; clientEmail: string }) {
config.gmail.privateKey = credentials.privateKey
config.gmail.clientEmail = credentials.clientEmail
}
/**
* Verifica si la configuración está completa
*/
export function isConfigured(): boolean {
return !!(config.gmail.privateKey && config.gmail.clientEmail)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment