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.

Tecnologías

NestJS

  • 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.

Arquitectura

1. Crear un nuevo proyecto en Google Cloud Platform

Para crear un nuevo proyecto:

  1. Clic en Create Project.
  2. En la ventana nuevo proyecto que aparece, ingrese un nombre de proyecto y seleccione una cuenta de facturación según corresponda.
  3. Ingresar la organización o carpeta principal en el cuadro ubicación.
  4. 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.

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.

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.

5. Crear un subscription

  • En el topic creado damos clic en los tres puntos finales y luego en Create subscription

6. Activar Gmail API en el proyecto

  • Ingresamos a la parte de APIs & Services, luego en Dashboard damos clic en ENABLE APIS AND SERVICES.

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.

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’,}}}
/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 {
/gmail/gmail.service.tsdecodeDataBase64(encodedData: string): string {const buffer = Buffer.from(encodedData, ‘base64’)return buffer.toString(‘utf-8’)}
/.envCLIENT_EMAIL=xxx@xxx.gserviceaccount.comPRIVATE_KEY=xxxxxxPRIVATE_KEY_ID=xxxxxPUB_SUB_TOPIC_NAME=projects/xxx/topics/xxxEMAIL_TO_READ=alex@tinkin.one
/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}}
/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}
/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}
/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
}
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}
  • 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’.
@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

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

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