Haz que el correo electrónico sea más práctico con los complementos de Google Workspace

1. Descripción general

En este codelab, usarás Google Apps Script para escribir un complemento de Google Workspace para Gmail que permita a los usuarios agregar datos de recibos de un correo electrónico a una hoja de cálculo directamente en Gmail. Cuando un usuario recibe un recibo por correo electrónico, abre el complemento, que obtiene automáticamente la información de gastos pertinente del correo electrónico. El usuario puede editar la información de los gastos y, luego, enviarla para registrarlos en una hoja de cálculo de Hojas de cálculo de Google.

Qué aprenderás

  • Crea un complemento de Google Workspace para Gmail con Google Apps Script
  • Analiza un correo electrónico con Google Apps Script
  • Interactúa con Hojas de cálculo de Google a través de Google Apps Script
  • Almacena valores del usuario con el servicio de propiedades de Google Apps Script

Requisitos

  • Acceso a Internet y un navegador web
  • Una Cuenta de Google
  • Algunos mensajes, preferentemente recibos de correo electrónico, en Gmail

2. Obtén el código de muestra

A medida que trabajes en este codelab, puede ser útil consultar una versión funcional del código que escribirás. El repositorio de GitHub contiene código de muestra que puedes usar como referencia.

Para obtener el código de muestra, ejecuta el siguiente comando desde la línea de comandos:

git clone https://github.com/googleworkspace/gmail-add-on-codelab.git

3. Crea un complemento básico

Comienza por escribir el código de una versión simple del complemento que muestre un formulario de gastos junto con un correo electrónico.

Primero, crea un proyecto nuevo de Apps Script y abre su archivo de manifiesto.

  1. Navega a script.google.com. Desde allí, puedes crear, administrar y supervisar tus proyectos de Apps Script.
  2. Para crear un proyecto nuevo, haz clic en Proyecto nuevo en la esquina superior izquierda. El proyecto nuevo se abre con un archivo predeterminado llamado Code.gs. Por ahora, deja Code.gs como está. Lo usarás más adelante.
  3. Haz clic en Untitled project, asígnale el nombre Expense It! y haz clic en Rename.
  4. A la izquierda, haz clic en Configuración del proyecto Configuración del proyecto.
  5. Selecciona la casilla de verificación Mostrar el archivo de manifiesto "appscript.json" en el editor.
  6. Haz clic en Editor Editor.
  7. Para abrir el archivo de manifiesto, haz clic en appscript.json a la izquierda.

En appscript.json, especifica los metadatos asociados con el complemento, como su nombre y los permisos que requiere. Reemplaza el contenido de appsscript.json con estos parámetros de configuración:

{
  "timeZone": "GMT",
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute"
  ],
  "gmail": {
    "name": "Expense It!",
    "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/receipt_black_24dp.png",
    "contextualTriggers": [{
      "unconditional": {
      },
      "onTriggerFunction": "getContextualAddOn"
    }],
    "primaryColor": "#41f470",
    "secondaryColor": "#94f441"
  }
}

Presta especial atención a la parte del manifiesto llamada contextualTriggers. Esta parte del manifiesto identifica la función definida por el usuario que se debe llamar cuando se activa el complemento por primera vez. En este caso, llama a getContextualAddOn, que obtiene detalles sobre el correo electrónico abierto y devuelve un conjunto de tarjetas para mostrar al usuario.

Para crear la función getContextualAddOn, sigue estos pasos:

  1. A la izquierda, mantén el puntero sobre Code.gs y, luego, haz clic en Menú Menú Más > Cambiar nombre.
  2. Escribe GetContextualAddOn y presiona la tecla Enter. Apps Script agrega automáticamente .gs al nombre del archivo, por lo que no es necesario que escribas una extensión de archivo. Si escribes GetContextualAddOn.gs, Apps Script le asignará el nombre GetContextualAddOn.gs.gs a tu archivo.
  3. En GetContextualAddOn.gs, reemplaza el código predeterminado por la función getContextualAddOn:
/**
 * Returns the contextual add-on data that should be rendered for
 * the current e-mail thread. This function satisfies the requirements of
 * an 'onTriggerFunction' and is specified in the add-on's manifest.
 *
 * @param {Object} event Event containing the message ID and other context.
 * @returns {Card[]}
 */
function getContextualAddOn(event) {
  var card = CardService.newCardBuilder();
  card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));

  var section = CardService.newCardSection();
  section.addWidget(CardService.newTextInput()
    .setFieldName('Date')
    .setTitle('Date'));
  section.addWidget(CardService.newTextInput()
    .setFieldName('Amount')
    .setTitle('Amount'));
  section.addWidget(CardService.newTextInput()
    .setFieldName('Description')
    .setTitle('Description'));
  section.addWidget(CardService.newTextInput()
    .setFieldName('Spreadsheet URL')
    .setTitle('Spreadsheet URL'));

  card.addSection(section);

  return [card.build()];
}

La interfaz de usuario de cada complemento de Google Workspace consta de tarjetas divididas en una o más secciones, cada una de las cuales contiene widgets que pueden mostrar y obtener información del usuario. La función getContextualAddOn crea una sola tarjeta que obtiene detalles sobre un gasto encontrado en un correo electrónico. La tarjeta tiene una sección que contiene campos de entrada de texto para los datos pertinentes. La función devuelve un array de las tarjetas del complemento. En este caso, el array devuelto incluye solo una tarjeta.

Antes de implementar el complemento Expense It!, necesitas un proyecto de Google Cloud Platform (GCP), que los proyectos de Apps Script usan para administrar autorizaciones, servicios avanzados y otros detalles. Para obtener más información, visita Proyectos de Google Cloud Platform.

Para implementar y ejecutar tu complemento, sigue estos pasos:

  1. Abre tu proyecto de GCP y copia su número.
  2. En tu proyecto de Apps Script, haz clic en Configuración del proyecto Configuración del proyecto a la izquierda.
  3. En "Proyecto de Google Cloud Platform (GCP)", haz clic en Cambiar proyecto.
  4. Ingresa el número de tu proyecto de GCP y, luego, haz clic en Establecer el proyecto.
  5. Haz clic en Implementar > Implementaciones de prueba.
  6. Asegúrate de que el tipo de implementación sea Complemento de Google Workspace. Si es necesario, en la parte superior del diálogo, haz clic en Habilitar los tipos de implementación Habilita los tipos de implementación y selecciona Complemento de Google Workspace como el tipo de implementación.
  7. Junto a Aplicaciones: Gmail, haz clic en Instalar.
  8. Haz clic en Listo.

Ahora puedes ver el complemento en tu carpeta Recibidos de Gmail.

  1. En tu computadora, abre Gmail.
  2. En el panel lateral derecho, se muestra la app de Expense It! Aparecerá el complemento Ícono de recibo de Expense It!. Es posible que debas hacer clic en Más complementos Más Complementos para encontrarlo.
  3. Abre un correo electrónico, de preferencia un recibo con gastos.
  4. Para abrir el complemento, haz clic en Expense It! en el panel lateral derecho. Ícono de recibo de Expense It!.
  5. Para otorgar acceso a Expense It! a tu Cuenta de Google, haz clic en Authorize Access y sigue las indicaciones.

El complemento muestra un formulario simple junto a un mensaje de Gmail abierto. Todavía no hace nada más, pero desarrollarás su funcionalidad en la siguiente sección.

Para ver las actualizaciones del complemento a medida que avanzas en este lab, solo debes guardar el código y actualizar Gmail. No se requieren implementaciones adicionales.

4. Acceder a mensajes de correo electrónico

Agrega código que recupere el contenido del correo electrónico y modulariza el código para una mejor organización.

Junto a Files, haz clic en Agregar Agregar un archivo > Script y crea un archivo llamado Cards. Crea un segundo archivo de secuencia de comandos llamado Helpers. Cards.gs crea la tarjeta y usa funciones de Helpers.gs para completar los campos del formulario según el contenido del correo electrónico.

Reemplaza el código predeterminado en Cards.gs por este código:

var FIELDNAMES = ['Date', 'Amount', 'Description', 'Spreadsheet URL'];

/**
 * Creates the main card users see with form inputs to log expenses.
 * Form can be prefilled with values.
 *
 * @param {String[]} opt_prefills Default values for each input field.
 * @param {String} opt_status Optional status displayed at top of card.
 * @returns {Card}
 */
function createExpensesCard(opt_prefills, opt_status) {
  var card = CardService.newCardBuilder();
  card.setHeader(CardService.newCardHeader().setTitle('Log Your Expense'));
  
  if (opt_status) {
    if (opt_status.indexOf('Error: ') == 0) {
      opt_status = '<font color=\'#FF0000\'>' + opt_status + '</font>';
    } else {
      opt_status = '<font color=\'#228B22\'>' + opt_status + '</font>';
    }
    var statusSection = CardService.newCardSection();
    statusSection.addWidget(CardService.newTextParagraph()
      .setText('<b>' + opt_status + '</b>'));
    card.addSection(statusSection);
  }
  
  var formSection = createFormSection(CardService.newCardSection(),
                                      FIELDNAMES, opt_prefills);
  card.addSection(formSection);
  
  return card;
}

/**
 * Creates form section to be displayed on card.
 *
 * @param {CardSection} section The card section to which form items are added.
 * @param {String[]} inputNames Names of titles for each input field.
 * @param {String[]} opt_prefills Default values for each input field.
 * @returns {CardSection}
 */
function createFormSection(section, inputNames, opt_prefills) {
  for (var i = 0; i < inputNames.length; i++) {
    var widget = CardService.newTextInput()
      .setFieldName(inputNames[i])
      .setTitle(inputNames[i]);
    if (opt_prefills && opt_prefills[i]) {
      widget.setValue(opt_prefills[i]);
    }
    section.addWidget(widget);
  }
  return section;
}

La función createExpensesCard toma un array de valores para completar previamente el formulario como un argumento opcional. La función puede mostrar un mensaje de estado opcional, que se muestra en rojo si el estado comienza con "Error:" y, de lo contrario, se muestra en verde. En lugar de agregar cada campo al formulario de forma manual, una función auxiliar llamada createFormSection itera el proceso de creación de widgets de entrada de texto, establece cada valor predeterminado con setValue y, luego, agrega los widgets a sus respectivas secciones en la tarjeta.

Ahora, reemplaza el código predeterminado en Helpers.gs por este código:

/**
 * Finds largest dollar amount from email body.
 * Returns null if no dollar amount is found.
 *
 * @param {Message} message An email message.
 * @returns {String}
 */
function getLargestAmount(message) {
  return 'TODO';
}

/**
 * Determines date the email was received.
 *
 * @param {Message} message An email message.
 * @returns {String}
 */
function getReceivedDate(message) {
  return 'TODO';
}

/**
 * Determines expense description by joining sender name and message subject.
 *
 * @param {Message} message An email message.
 * @returns {String}
 */
function getExpenseDescription(message) {
  return 'TODO';
}

/**
 * Determines most recent spreadsheet URL.
 * Returns null if no URL was previously submitted.
 *
 * @returns {String}
 */
function getSheetUrl() {
  return 'TODO';
}

getContextualAddon llama a las funciones en Helpers.gs para determinar los valores precompletados en el formulario. Por ahora, estas funciones solo devolverán la cadena "TODO" porque implementarás la lógica de precompletado en un paso posterior.

A continuación, actualiza el código en GetContextualAddon.gs para que aproveche el código en Cards.gs y Helpers.gs. Reemplaza el código de GetContextualAddon.gs por el siguiente:

/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Returns the contextual add-on data that should be rendered for
 * the current e-mail thread. This function satisfies the requirements of
 * an 'onTriggerFunction' and is specified in the add-on's manifest.
 *
 * @param {Object} event Event containing the message ID and other context.
 * @returns {Card[]}
 */
function getContextualAddOn(event) {
  var message = getCurrentMessage(event);
  var prefills = [getReceivedDate(message),
                  getLargestAmount(message),
                  getExpenseDescription(message),
                  getSheetUrl()];
  var card = createExpensesCard(prefills);

  return [card.build()];
}

/**
 * Retrieves the current message given an action event object.
 * @param {Event} event Action event object
 * @return {Message}
 */
function getCurrentMessage(event) {
  var accessToken = event.messageMetadata.accessToken;
  var messageId = event.messageMetadata.messageId;
  GmailApp.setCurrentMessageAccessToken(accessToken);
  return GmailApp.getMessageById(messageId);
}

Ten en cuenta la nueva función getCurrentMessage, que usa el evento proporcionado por Gmail para leer el mensaje que el usuario tiene abierto actualmente. Para que esta función funcione, agrega un alcance adicional al manifiesto del guion que permita el acceso de solo lectura a los mensajes de Gmail.

En appscript.json, actualiza oauthScopes para que también solicite el permiso https://www.googleapis.com/auth/gmail.addons.current.message.readonly.

"oauthScopes": [
  "https://www.googleapis.com/auth/gmail.addons.execute",
   "https://www.googleapis.com/auth/gmail.addons.current.message.readonly"
],

En Gmail, ejecuta el complemento y autoriza el acceso de Expense It! para ver los mensajes de correo electrónico. Los campos del formulario ahora están completados previamente con "TODO".

5. Interactúa con Hojas de cálculo de Google

El complemento Expense It! tiene un formulario para que el usuario ingrese detalles sobre un gasto, pero esos detalles no se envían a ningún lugar. Agreguemos un botón que envíe los datos del formulario a una hoja de cálculo de Google.

Para agregar un botón, usaremos la clase ButtonSet. Para interactuar con Hojas de cálculo de Google, usaremos el servicio de Hojas de cálculo de Google.

Modifica createFormSection para que muestre un botón con la etiqueta "Enviar" como parte de la sección del formulario de la tarjeta. Debes seguir estos pasos:

  1. Crea un botón de texto con CardService.newTextButton() y etiquétalo como “Enviar” con CardService.TextButton.setText().
  2. Diseña el botón de modo que, cuando se haga clic en él, se llame a la siguiente acción submitForm a través de CardService.TextButton.setOnClickAction():
/**
 * Logs form inputs into a spreadsheet given by URL from form.
 * Then displays edit card.
 *
 * @param {Event} e An event object containing form inputs and parameters.
 * @returns {Card}
 */
function submitForm(e) {
  var res = e['formInput'];
  try {
    FIELDNAMES.forEach(function(fieldName) {
      if (! res[fieldName]) {
        throw 'incomplete form';
      }
    });
    var sheet = SpreadsheetApp
      .openByUrl((res['Spreadsheet URL']))
      .getActiveSheet();
    sheet.appendRow(objToArray(res, FIELDNAMES.slice(0, FIELDNAMES.length - 1)));
    return createExpensesCard(null, 'Logged expense successfully!').build();
  }
  catch (err) {
    if (err == 'Exception: Invalid argument: url') {
      err = 'Invalid URL';
      res['Spreadsheet URL'] = null;
    }
    return createExpensesCard(objToArray(res, FIELDNAMES), 'Error: ' + err).build();
  }
}

/**
 * Returns an array corresponding to the given object and desired ordering of keys.
 *
 * @param {Object} obj Object whose values will be returned as an array.
 * @param {String[]} keys An array of key names in the desired order.
 * @returns {Object[]}
 */
function objToArray(obj, keys) {
  return keys.map(function(key) {
    return obj[key];
  });
}
  1. Crea un widget de conjunto de botones con CardService.newButtonSet() y agrega tu botón de texto al conjunto de botones con CardService.ButtonSet.addButton().
  2. Agrega el widget de conjunto de botones a la sección del formulario de la tarjeta con CardService.CardSection.addWidget().

Con solo unas pocas líneas de código, podemos abrir una hoja de cálculo por su URL y, luego, agregarle una fila de datos. Ten en cuenta que las entradas del formulario se pasan a la función como parte del evento e, y verificamos que el usuario haya proporcionado todos los campos. Si no se producen errores, creamos una tarjeta de gastos en blanco con un estado favorable. En caso de que detectemos un error, devolvemos la tarjeta original completada junto con el mensaje de error. La función de ayuda objToArray facilita la conversión de las respuestas del formulario en un array, que luego se puede agregar a la hoja de cálculo.

Por último, vuelve a actualizar la sección oauthScopes en appsscript.json y solicita el alcance https://www.googleapis.com/auth/spreadsheets. Cuando se autoriza este alcance, el complemento puede leer y modificar las Hojas de cálculo de Google de un usuario.

"oauthScopes": [
  "https://www.googleapis.com/auth/gmail.addons.execute",
  "https://www.googleapis.com/auth/gmail.addons.current.message.readonly",
  "https://www.googleapis.com/auth/spreadsheets"
],

Si aún no creaste una hoja de cálculo nueva, crea una en https://docs.google.com/spreadsheets/.

Ahora, vuelve a ejecutar el complemento y prueba enviar el formulario. Asegúrate de ingresar la URL completa de tu URL de destino en el campo del formulario URL de la hoja de cálculo.

6. Almacena valores con el servicio de propiedades

A menudo, los usuarios registran muchos gastos en la misma hoja de cálculo, por lo que sería conveniente ofrecer la URL de la hoja de cálculo más reciente como valor predeterminado en la tarjeta. Para conocer la URL de la hoja de cálculo más reciente, deberemos almacenar esa información cada vez que se use el complemento.

El servicio de Properties nos permite almacenar pares clave-valor. En nuestro caso, una clave razonable sería "SPREADSHEET_URL", mientras que el valor sería la URL en sí. Para almacenar ese valor, deberás modificar submitForm en Cards.gs de modo que la URL de la hoja de cálculo se almacene como una propiedad cuando se agregue una fila nueva a la hoja.

Ten en cuenta que las propiedades pueden tener uno de los tres siguientes alcances: secuencia de comandos, usuario o documento. El alcance del documento no se aplica a los complementos de Gmail, aunque es pertinente para un tipo de complemento independiente cuando se almacena información específica de un documento o una hoja de cálculo de Google en particular. En el caso de nuestro complemento, el comportamiento deseado es que una persona vea su hoja de cálculo más reciente (en lugar de la de otra persona) como la opción predeterminada en el formulario. Por lo tanto, seleccionamos el alcance de usuario en lugar del alcance de secuencia de comandos.

Usa PropertiesService.getUserProperties().setProperty() para almacenar la URL de la hoja de cálculo. Agrega el siguiente código a submitForm en Cards.gs:

PropertiesService.getUserProperties().setProperty('SPREADSHEET_URL', 
    res['Spreadsheet URL']);

Luego, modifica la función getSheetUrl en Helpers.gs para que devuelva la propiedad almacenada de modo que el usuario vea la URL más reciente cada vez que use el complemento. Usa PropertiesService.getUserProperties().getProperty() para obtener el valor de la propiedad.

/**
 * Determines most recent spreadsheet URL.
 * Returns null if no URL was previously submitted.
 *
 * @returns {String}
 */
function getSheetUrl() {
  return PropertiesService.getUserProperties().getProperty('SPREADSHEET_URL');
}

Por último, para acceder al servicio de Property, también se deberá autorizar la secuencia de comandos. Agrega el alcance https://www.googleapis.com/auth/script.storage al manifiesto como antes para permitir que tu complemento lea y escriba información de propiedades.

7. Analiza el mensaje de Gmail

Para ahorrarles tiempo a los usuarios, completemos previamente el formulario con información relevante sobre el gasto que se encuentra en el correo electrónico. Ya creamos funciones en Helpers.gs que cumplen este rol, pero, hasta ahora, solo devolvimos "TODO" para la fecha, el importe y la descripción del gasto.

Por ejemplo, podemos obtener la fecha en que se recibió el correo electrónico y usarla como valor predeterminado para la fecha del gasto.

/**
 * Determines date the email was received.
 *
 * @param {Message} message - The message currently open.
 * @returns {String}
 */
function getReceivedDate(message) {
  return message.getDate().toLocaleDateString();
}

Implementa las dos funciones restantes:

  1. getExpenseDescription podría implicar unir el nombre del remitente y el asunto del mensaje, aunque existen formas más sofisticadas de analizar el cuerpo del mensaje y ofrecer una descripción aún más precisa.
  2. Para getLargestAmount, considera buscar símbolos específicos asociados con el dinero. Los recibos suelen tener varios valores, como impuestos y otras tarifas. Piensa cómo podrías identificar la cantidad correcta. Las expresiones regulares también pueden ser útiles.

Si necesitas más inspiración, explora la documentación de referencia de GmailMessage o consulta el código de solución que descargaste al principio del codelab. Una vez que hayas diseñado tus propias implementaciones para todas las funciones en Helpers.gs, prueba tu complemento. Abre los recibos y comienza a registrarlos en una hoja de cálculo.

8. Borra el formulario con acciones de tarjetas

¿Qué sucede si Expense It! identifica erróneamente un gasto en un correo electrónico abierto y completa previamente el formulario con información incorrecta? El usuario borra el formulario. La clase CardAction nos permite especificar una función a la que se llama cuando se hace clic en la acción. Usémoslo para brindarles a los usuarios una forma rápida de borrar el formulario.

Modifica createExpensesCard de modo que la tarjeta que devuelva tenga una acción de tarjeta etiquetada como "Borrar formulario" y, cuando se haga clic en ella, llame a la siguiente función clearForm, que puedes pegar en Cards.gs. Deberás pasar opt_status como un parámetro llamado "Status" a la acción para asegurarte de que, cuando se borre el formulario, permanezca el mensaje de estado. Ten en cuenta que los parámetros opcionales para las acciones deben ser del tipo Object.<string, string>, por lo que, si opt_status no está disponible, debes pasar {'Status' : ''}.

/**
 * Recreates the main card without prefilled data.
 *
 * @param {Event} e An event object containing form inputs and parameters.
 * @returns {Card}
 */
function clearForm(e) {
  return createExpensesCard(null, e['parameters']['Status']).build();
}

9. Crea una hoja de cálculo

Además de usar Google Apps Script para editar una hoja de cálculo existente, puedes crear una hoja de cálculo completamente nueva de forma programática. En el caso de nuestro complemento, permitiremos que el usuario cree una hoja de cálculo para los gastos. Para comenzar, agrega la siguiente sección de tarjeta a la tarjeta que devuelve createExpensesCard.

var newSheetSection = CardService.newCardSection();
var sheetName = CardService.newTextInput()
  .setFieldName('Sheet Name')
  .setTitle('Sheet Name');
var createExpensesSheet = CardService.newAction()
  .setFunctionName('createExpensesSheet');
var newSheetButton = CardService.newTextButton()
  .setText('New Sheet')
  .setOnClickAction(createExpensesSheet);
newSheetSection.addWidget(sheetName);
newSheetSection.addWidget(CardService.newButtonSet().addButton(newSheetButton));
card.addSection(newSheetSection);

Ahora, cuando el usuario haga clic en el botón "Hoja nueva", el complemento generará una nueva hoja de cálculo con formato y una fila de encabezado inmovilizada para que siempre esté visible. El usuario especifica un título para la nueva hoja de cálculo en el formulario, aunque incluir un valor predeterminado en caso de que el formulario esté en blanco podría ser una buena opción. En tu implementación de createExpensesSheet, devuelve una tarjeta casi idéntica a la existente, con la adición de un mensaje de estado adecuado y el campo de URL completado previamente con la URL de la nueva hoja de cálculo.

10. ¡Felicitaciones!

Diseñaste e implementaste correctamente un complemento de Gmail que encuentra un gasto en un correo electrónico y ayuda a los usuarios a registrarlo en una hoja de cálculo en cuestión de segundos. Usaste Google Apps Script para interactuar con varias APIs de Google y persistir datos entre varias ejecuciones del complemento.

Posibles mejoras

Deja que tu imaginación te guíe mientras mejoras Expense It!, pero aquí tienes algunas ideas para crear un producto aún más útil:

  • Vínculo a la hoja de cálculo una vez que el usuario haya registrado un gasto
  • Agregar la capacidad de editar o deshacer el registro de un gasto
  • Integra APIs externas para permitir que los usuarios realicen pagos y soliciten dinero

Más información