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 relevante de los gastos del correo electrónico. El usuario puede editar la información de los gastos y, luego, enviarla para registrarla en una hoja de cálculo de Google Sheets.

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
  • Almacenar valores de usuario con el servicio de propiedades de Google Apps Script

Requisitos

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

2. Obtén el código de muestra

A medida que trabajas en este codelab, podría resultarte útil hacer referencia a 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, desde la línea de comandos, ejecuta lo siguiente:

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

3. Cómo crear un complemento básico

Comienza escribiendo el código para una versión simple del complemento que muestra un formulario de gastos junto con un correo electrónico.

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

  1. Navega a script.google.com. Desde aquí, puedes crear, administrar y supervisar tus proyectos de Apps Script.
  2. Para crear un proyecto nuevo, en la esquina superior izquierda, haz clic en Proyecto nuevo. Se abrirá el proyecto nuevo con un archivo predeterminado llamado Code.gs. Por ahora, deja Code.gs solo; trabajarás con él más tarde.
  3. Haz clic en Proyecto sin título, asígnale el nombre Gasto. y haz clic en Cambiar nombre.
  4. A la izquierda, haz clic en Configuración del proyecto Configuración del proyecto.
  5. Selecciona el archivo Show "appscript.json" archivo de manifiesto en el editor".
  6. Haz clic en Editor Editor.
  7. Para abrir el archivo de manifiesto, a la izquierda, haz clic en appscript.json.

En appscript.json, especifica los metadatos asociados con el complemento, como su nombre y los permisos que requiere. Reemplaza el contenido de appsscript.json por 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 denominada contextualTriggers. Esta parte del manifiesto identifica la función definida por el usuario a la 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 muestra un conjunto de tarjetas para mostrarle 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 de tu archivo para que no tengas que escribir una extensión de archivo. Si escribes GetContextualAddOn.gs, Apps Script asigna 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 con widgets que pueden mostrar información del usuario y obtener información de ella. La función getContextualAddOn crea una sola tarjeta que obtiene los detalles de un gasto encontrado en un correo electrónico. La tarjeta tiene una sección que contiene campos de entrada de texto para los datos relevantes. La función muestra un array de las tarjetas del complemento. En este caso, el array que se muestra incluye solo una tarjeta.

Antes de implementar 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 de proyecto.
  2. En el lado izquierdo de tu proyecto de Apps Script, haz clic en Configuración del proyecto Configuración del proyecto.
  3. En “Proyecto de Google Cloud Platform (GCP)”, haz clic en Cambiar proyecto.
  4. Ingresa el número del proyecto de GCP y, luego, haz clic en Configurar 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 tipos de implementación Habilitar tipos de implementaciones 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 la carpeta Recibidos de Gmail.

  1. En tu computadora, abre Gmail.
  2. En el panel de la derecha, está la opción Expense It! Aparecerá el complemento ¡Gástalo! ícono de recibo. Es posible que debas hacer clic en Más complementos Más Complementos para encontrarla.
  3. Abre un correo electrónico, preferentemente un recibo con los gastos.
  4. Para abrir el complemento, en el panel lateral derecho, haz clic en Expense It! ¡Gástalo! ícono de recibo.
  5. ¡Da los gastos! acceso a tu Cuenta de Google haciendo clic en Autorizar acceso 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 compilarás su funcionalidad en la próxima sección.

Para ver actualizaciones de tu complemento a medida que avanzas en este lab, solo tienes que guardar tu código y actualizar Gmail. No se necesitan implementaciones adicionales.

4. Acceder a los mensajes de correo electrónico

Agrega código que recupere el contenido del correo electrónico y modulariza el código para un poco más de organización.

Junto a Archivos, 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 de Cards.gs con 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 aparecerá en color rojo si el estado comienza con “Error:”, y, de lo contrario, aparecerá de color verde. En lugar de agregar cada campo al formulario de forma manual, una función auxiliar llamada createFormSection recorre en bucle 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 de Helpers.gs con 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 de Helpers.gs para determinar los valores completados previamente en el formulario. Por ahora, estas funciones solo mostrarán la string "TODO" porque implementarás la lógica de completado previo 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 con este código:

/**
 * 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);
}

Observa la nueva función getCurrentMessage, que usa el evento proporcionado por Gmail para leer el mensaje abierto del usuario. Para que esta función se ejecute correctamente, agrega un alcance adicional al manifiesto de secuencia de comandos que permite 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 tu complemento y autoriza el acceso para Expense It! para ver los mensajes de correo electrónico. Los campos del formulario ahora aparecerán con la palabra "TODO".

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

¡Los gastos! complemento tiene un formulario para que el usuario ingrese detalles sobre un gasto, pero esos detalles no tienen adonde ir. 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 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 etiqueta el botón como “Enviar”. usando 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 botón configurado 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 unas pocas líneas de código, podemos abrir una hoja de cálculo por su URL y, luego, agregar una fila de datos a esa hoja. 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. Suponiendo que no se produzcan errores, creamos una tarjeta de gastos en blanco con un estado favorable. En caso de que detectemos un error, devolvemos la tarjeta completada original junto con el mensaje de error. La función auxiliar 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, actualiza la sección oauthScopes en appsscript.json nuevamente y solicita el permiso https://www.googleapis.com/auth/spreadsheets. Cuando se autoriza, este permiso permite que el complemento lea y modifique 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 tienes una hoja de cálculo nueva, crea una en https://docs.google.com/spreadsheets/.

Ahora, vuelve a ejecutar el complemento y envía el formulario. Asegúrese de ingresar la URL completa de su URL de destino en el campo del formulario URL de la hoja de cálculo.

6. Almacena valores con el servicio Propiedades

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

El servicio Properties nos permite almacenar pares clave-valor. En nuestro caso, una clave razonable sería "SPREADSHEET_URL" mientras que el valor sería la propia URL. Para almacenar este 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 agregues una nueva fila a la hoja.

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

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

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

Luego, modifica la función getSheetUrl en Helpers.gs para mostrar 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 propiedades, también se debe 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 ahorrar tiempo vamos a completar automáticamente el formulario con información relevante sobre los gastos del correo electrónico. Ya creamos funciones en Helpers.gs que cumplen este rol, pero hasta ahora solo mostramos "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 el 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 puede implicar unir el nombre del remitente y el asunto del mensaje, aunque existen formas más sofisticadas de analizar el cuerpo del mensaje y brindar una descripción aún más precisa.
  2. Para getLargestAmount, busca símbolos específicos asociados con el dinero. Los recibos suelen tener varios valores enumerados, como impuestos y otras tarifas. Piensa en 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 revisa el código de solución que descargaste al comienzo del codelab. Una vez que hayas diseñado tus propias implementaciones para todas las funciones de Helpers.gs, prueba tu complemento. Abre los recibos y comienza a registrarlos en una hoja de cálculo.

8. Borrar el formulario con acciones de la tarjeta

¿Qué sucede si lo gastas? identifica erróneamente un gasto en un correo electrónico abierto y precompleta 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. Vamos a usarlo para que el usuario tenga una forma rápida de borrar el formulario.

Modifica createExpensesCard para que la tarjeta que muestra tenga una acción de tarjeta etiquetada como "Borrar formulario" y, cuando se hace clic en él, se llama 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 garantizar que, cuando se borre el formulario, el mensaje de estado permanezca en el mismo lugar. 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. Crear 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 manera programática. Para nuestro complemento, vamos a permitir que el usuario cree una hoja de cálculo con los gastos. Para comenzar, agrega la siguiente sección de la tarjeta a la 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 la "Nueva hoja" el complemento genera una hoja de cálculo nueva con un formato y una fila de encabezado que se inmoviliza de modo que siempre esté visible. El usuario especifica un título para la nueva hoja de cálculo en el formulario, aunque puede ser una buena opción incluir un valor predeterminado en caso de que el formulario esté en blanco. En tu implementación de createExpensesSheet, muestra una tarjeta casi idéntica a la existente, con un mensaje de estado apropiado y precompleta el campo de URL 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. Utilizaste Google Apps Script para interactuar con varias APIs de Google y conservar datos entre varias ejecuciones del complemento.

Posibles mejoras

Deja que tu imaginación te guíe mientras mejoras los gastos, pero estas son algunas ideas para hacer un producto aún más útil:

  • Vínculo a la hoja de cálculo una vez que el usuario haya registrado un gasto
  • Agrega 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