Cómo utilizar push notifications con Gmail API

Problema

En un proyecto de Tinkin teníamos el siguiente problema: En un correo de gmail llegaban múltiples mensajes diarios con información que debemos enviar a un backend para luego ser procesada. Entonces cómo puede saber nuestro backend cuando llega un nuevo correo y obtener esa nueva información para procesarla.

Solución

La siguiente solución está pensada para cuando llegue un nuevo mensaje a nuestro inbox, gmail envíe una push notification a nuestro backend y que la información contenida se procese lo más rápido posible.

Gmail API proporciona Push Notifications del servidor las cuales permiten observar los cambios en el buzón de un correo de Gmail. Se puede utilizar esta función para mejorar el rendimiento en aplicaciones y eliminar costos relacionados al sondeo para determinar si ha cambiado algo en el buzón del correo.

Cada vez que cambia algo en el buzón de un correo de Gmail, como puede ser la entrada de un nuevo correo, eliminación de un correo, lectura de un correo o cualquier otra acción relacionada con un correo esta va a ser notificada a un servidor backend para su posterior procesamiento.

Tecnologías

NestJS

NestJS es un framework de Node.js para crear aplicaciones del lado del servidor eficientes, confiables y escalables. Utilizaremos NestJS como nuestro backend el cual va a recibir las push notification y procesará la información de los nuevos mensajes recibidos.

Gmail API

La API de Gmail es una API RESTful que se puede utilizar para acceder a los buzones de correos de Gmail para leer correos, enviar correos, crear labels, enviar notificaciones y varias acciones más que se detallan en su documentación. Es compatible con varios lenguajes de programación como Java, JavaScript y Python.

Utilizaremos la API de Gmail para enviar las push notifications así como para leer los correos electrónicos y marcarlos como leídos desde el backend.

Pub/Sub

Pub/Sub es un servicio de Google Cloud Platform el cual permite que varios servicios se comuniquen de forma asincrónica, con latencias en el orden de los 100 milisegundos.

Conceptos Básicos:

  • Topic: un recurso nombrado al que los publishers envían mensajes.
  • Subscription: un recurso que representa el flujo de mensajes de un topic único y específico, que se entregará a la aplicación que se suscribe.
  • Publisher: una aplicación que crea y envía mensajes a un topic.
  • Subscriber: una aplicación que se suscribe a un topic para recibir mensajes de él.
  • Push and pull: son dos dos métodos de entrega de mensajes. Un subscriber puede recibir mensajes por push es decir se envía dicho mensaje a un endpoint o webhook y por pull el subscriber es el que tiene que extraer los mensajes desde el servicio.

Utilizaremos Pub/Sub para crear un sistema de publishers y subscribers con esto el publishers que vendría a ser Gmail enviará una notificación a nuestro subscriber que vendría a ser un endpoint en el backend al cual le llegará dicha notificación.

Arquitectura

1. Crear un nuevo proyecto en Google Cloud Platform

Para crear un nuevo proyecto:

  1. Ir a la página de Google Cloud Platform.
  2. Clic en Create Project.
  3. En la ventana nuevo proyecto que aparece, ingrese un nombre de proyecto y seleccione una cuenta de facturación según corresponda.
  4. Ingresar la organización o carpeta principal en el cuadro ubicación.
  5. Cuando haya terminado de ingresar los detalles del nuevo proyecto, haga clic en crear.

2. Habilita la API de Pub/Sub para ese proyecto

  • Ingresar a la siguiente página que indica cómo realizar tareas básicas para activar Pub/Sub usando Google Cloud Console y presionamos en Set up a project.
  • Seleccionamos el proyecto que se creó anteriormente y damos en Next.
  • Por último, si todo salió bien, debe mostrar un pop-up como el siguiente. Con esto ya tenemos habilitado la API de Pub/Sub.

3. Crear un Topic

Un Topic es un recurso con un identificador al que los publishers (en este caso la API de Gmail) envían mensajes y este se encargará de distribuir dichos mensajes a los subscribers.

  • Ingresamos a la siguiente página para seguir con las configuraciones del Pub/Sub y presionamos en Go to the Pub/Sub topics page.
  • Luego damos clic en CREATE TOPIC e ingresamos un Topic ID el cual va a ser simplemente un identificador para ese Topic y damos clic en Create Topic.
  • Con esto ya tenemos el Topic creado y si todo salió bien, se verá el topic de la siguiente manera.

4. Otorga permisos de publisher para la API de Gmail en el Topic

  • Seleccionamos el Topic creado y en la ventana derecha damos clic en PERMISSIONS y luego clic en ADD MEMBER.
  • Se abre la siguiente ventana e ingresamos el siguiente correo dentro de New members gmail-api-push@system.gserviceaccount.com (es un correo por default que nos da la API de Gmail para utilizar las push notifications), en la parte de Role buscamos Pub/Sub y seleccionamos Pub/Sub Publisher y finalmente damos clic en Save.
  • Y con esto ya se ha otorgado los permisos a la API de Gmail para hacer publicaciones y se debería ver de la siguiente manera.

5. Crear un subscription

  • En el topic creado damos clic en los tres puntos finales y luego en Create subscription
  • Ingresamos un ID para la subscription y en Delivery type seleccionamos de tipo Push e ingresamos la URL del endpoint al cual queremos que lleguen las notificaciones de nuevos cambios en el inbox.
  • Luego en Expiration period seleccionamos Never expire para que nunca expire la suscripción y en Acknowledgement deadline ingresamos 60 segundos. Estos 60 segundos indica cuánto tiempo espera el Pub/Sub a que el suscriptor confirme que recibió la notificación antes de reenviar.
  • Finalmente le damos en CREATE y con esto ya tenemos creado el Subscriber.

6. Activar Gmail API en el proyecto

  • Ingresamos a la parte de APIs & Services, luego en Dashboard damos clic en ENABLE APIS AND SERVICES.
  • Buscamos Gmail API.
  • Finalmente le damos en ENABLE y con esto ya tenemos activado Gmail API en nuestro proyecto.

7. Crear las credenciales de autenticación

  • Ingresamos a la parte de APIs & Services y en la pestaña de Credentials seleccionamos CREATE CREDENTIALS y luego en Service account.
  • Nos llevará a la siguiente ventana en donde ingresamos un Service account name y le damos en CREATE AND CONTINUE.
  • Esta parte la dejamos vacía y le damos en CONTINUE.
  • Esta parte igual la dejamos vacía y le damos en DONE y con esto ya tenemos la credencial creada.
  • Ahora ingresamos a la credencial que creamos.
  • Luego vamos a la parte de SHOW DOMAIN-WIDE DELEGATION y le damos clic.
  • Luego le damos check en Enable Google Workspace y luego en SAVE.
  • Y aparecerá el siguiente Cliente ID que lo debemos guardar para los siguientes pasos.
  • Ahora vamos a la parte de KEYS y le damos en ADD KEY luego en Create new Key.
  • Seleccionamos de tipo JSON y le damos en CREATE.
  • Aparecerá el siguiente mensaje y automáticamente se descargara un archivo .json con nuestras credenciales que nos servirán para autenticarnos con Google desde nuestro backend.
  • Finalmente debemos ir a la siguiente página y seguir los pasos descritos (para esto se necesita un rol o permisos de Super Admin). Aquí debemos utilizar el CLIENTE ID que copiamos en el paso anterior e ingresamos el siguiente scope https://www.googleapis.com/auth/gmail.modify para poder modificar los correos entrantes de Gmail desde el backend.

8. Conexión al Backend

  • Creamos un controlador al cual llegará la push notification de tipo POST y luego llamamos al servicio para procesar la información.
/gmail/gmail.controller.ts
import { Controller, Post } from ‘@nestjs/common’
import { DEFAULT_RESPONSE } from ‘../util/constant’import { GmailService } from ‘./gmail.service’@Controller(‘gmail’)export class GmailController {constructor(private gmailService: GmailService) {}@Post(‘receive-push-notification’)async receivePushNotification(): Promise<{success: booleanmessage: string}> {await this.gmailService.processGmailNotification()return {success: true,message: ‘Información Recibida’,}}}
  • Instalamos la dependencia de ‘googleapis’ para utilizar la API de Gmail así como ‘@nestjs/schedule’ para utilizar los Cron jobs de Nest. Todo esto dentro de un servicio.
/gmail/gmail.service.ts
import { Injectable, Logger } from ‘@nestjs/common’
import { gmail_v1, google } from ‘googleapis’import { Cron, CronExpression } from ‘@nestjs/schedule’@Injectable()export class GmailService {
  • Creamos un método simple para decodificar datos en base64 que utilizaremos en los siguientes métodos más adelante.
/gmail/gmail.service.tsdecodeDataBase64(encodedData: string): string {const buffer = Buffer.from(encodedData, ‘base64’)return buffer.toString(‘utf-8’)}
  • Ahora creamos un método de autenticación y autorización para poder leer y modificar los correos de Gmail. Dentro de este método utilizaremos las credenciales dentro del archivo .JSON que se descargó anteriormente así como el nombre del topic creado dentro del pub/sub y las pondremos en un archivo .env.

💡 La variable EMAIL_TO_READ contiene la cuenta de gmail desde la cual se leerán y modificarán los nuevos correos que lleguen.

/.envCLIENT_EMAIL=xxx@xxx.gserviceaccount.comPRIVATE_KEY=xxxxxxPRIVATE_KEY_ID=xxxxxPUB_SUB_TOPIC_NAME=projects/xxx/topics/xxxEMAIL_TO_READ=alex@tinkin.one

💡La PRIVATE_KEY la codificamos en base64 para evitar problemas de caracteres al momento de utilizarla dentro del método de autenticación.

/gmail/gmail.service.tsasync auth(): Promise<gmail_v1.Gmail> {// scopes para poder modificar y leer un correo de gmailconst scopes = [‘https://www.googleapis.com/auth/gmail.modify']// Decodificamos la PRIVATE_KEYconst decodedKey = this.decodeDataBase64(process.env.PRIVATE_KEY)try {// Utilizamos el método de autenticación de google con JWT y// retornamos la autorización de gmail.const jwtClient = new google.auth.JWT(process.env.CLIENT_EMAIL,null,decodedKey,scopes,process.env.EMAIL_TO_READ,process.env.PRIVATE_KEY_ID,)await jwtClient.authorize()google.options({ auth: jwtClient })return google.gmail({ version: ‘v1’ })} catch (error) {return error}}
  • Creamos un método para listar los nuevos correos que tenemos en el INBOX. Con este método nos aseguramos de listar siempre el último correo que se encuentre en INBOX, que no esté leído y que provenga en este caso de test@email.com.

💡 Este método no trae la información contenida en el correo simplemente traer un arreglo con los IDs de los nuevos correos.

/gmail/gmail.service.tsasync listEmails( gmail: gmail_v1.Gmail): Promise<gmail_v1.Schema$Message[]> {const messagesList = await gmail.users.messages.list({userId: process.env.EMAIL_TO_READ,// Número máximo de correos que se va a traer (en este caso 1)maxResults: 1,// Label desde donde se van a traer el correolabelIds: [‘INBOX’],// Query para solo traer los mensajes de test@email.com// que no estén en read, chat, draft y tampoco en trash.q: `from:test@email.com -in:read -in:chat -in:draft -in:trash`,})return messagesList.data.messages}
  • Ahora sí con la siguiente función traemos toda la información que contiene el correo según el ID pasado como parámetro.
/gmail/gmail.service.tsasync getUserEmail(gmail: gmail_v1.Gmail,messageId: string,): Promise<gmail_v1.Schema$MessagePart> {const emailClient = await gmail.users.messages.get({userId: process.env.EMAIL_TO_READ,id: messageId,})return emailClient.data.payload}
  • Como penúltimo paso creamos un método que marcará el correo como leído.
/gmail/gmail.service.tsasync markEmailAsRead(gmail: gmail_v1.Gmail,messageId: string,): Promise<gmail_v1.Schema$Message> {const modify = await gmail.users.messages.modify({userId: process.env.EMAIL_TO_READ,id: messageId,requestBody: {removeLabelIds: [‘UNREAD’],},})
return modify.data
}
  • Finalmente creamos un método principal que retornará el body que contiene toda la información del correo en texto plano.
async processGmailNotification() {let emailData// Utilizamos el método de autenticaciónconst gmail = await this.auth()// Listamos el último correo de INBOXconst listEmail = await this.listEmails(gmail)if (listEmail.length) {// Obtenemos el ID del correoconst messageId = listEmail.pop().id// Obtenemos la información del nuevo correoconst emailClient = await this.getUserEmail(gmail, messageId)// Decodificamos la información que llega en el body en base64emailData = this.decodeDataBase64(emailClient.body.data)// Marcamos el correo como leídoawait this.markEmailAsRead(gmail, messageId)}// Finalmente retornamos la información del correo ya en texto plano// para poder utilizarla en otros métodos.return emailData}
  • Ahora para que todo esto funcione correctamente necesitamos ejecutar el siguiente método watch() al menos una vez al día que es lo recomendable según la documentación de Gmail API.
  • Este método permite configurar la cuenta de Gmail desde la cual se enviará notificaciones al Topic. Para hacerlo, se proporciona el nombre del Topic creado anteriormente y opcional los labels para filtrar (opcionales). Por ejemplo, para recibir una notificación cada vez que se realice un cambio en el INBOX enviamos en la propiedad labelIds un string que contiene la palabra ‘INBOX’.

💡 Para este caso utilizamos un Cron Job de Nest el cual ejecutara este método todos los días a la media noche y así evitamos que la cuenta de gmail se desuscriba y deje de enviar notificaciones al Topic y por consecuencia dejemos de recibir las notificaciones push a nuestro backend.

@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)async watchNotifications(): Promise<number> {const gmail = await this.auth()const watch = await gmail.users.watch({userId: process.env.EMAIL_TO_READ,requestBody: {labelIds: [‘INBOX’],topicName: process.env.PUB_SUB_TOPIC_NAME,},})return watch.status}

Conclusión

Gmail API proporciona una manera fácil de interactuar con las cuentas de Gmail a través de su RESTFul API y esto junto al Pub/Sub lo convierte en una herramienta muy poderosa cuando se requiere notificar nuevos cambios de una cuenta de gmail y enviarlos a un endpoint o webhook en específico. Con esta arquitectura delegamos todo el trabajo a la API de Gmail y al Pub/Sub por lo tanto liberamos al backend la responsabilidad de realizar peticiones cada minuto para saber si algo cambio en la cuenta de Gmail y ahora simplemente esperamos una notificación y la procesamos.

Referencias

Using OAuth 2.0 for Server to Server Applications | Google Identity

What Is Pub/Sub? | Cloud Pub/Sub Documentation | Google Cloud

Push Notifications | Gmail API | Google Developers

Autor: Alex Arevalo

--

--

--

Hey! We are Tinkin, a startup that supports other startups, advising them from the definition of their MVP to the moon.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tinkin

Tinkin

Hey! We are Tinkin, a startup that supports other startups, advising them from the definition of their MVP to the moon.

More from Medium

Change is Coming: API Authentication

A door to a highly restricted area.

About Cloudflare proxy status and Let’s encrypt and too many redirects error

Redis Sentinel implementation for SpringBoot application

Getting started with using Zoom APIs

Getting started with using Zoom APIs