教程:使用 Cloud Functions 函数通过 SendGrid 发送电子邮件

本教程演示了如何使用 Cloud Functions 函数通过 SendGrid 平台发送电子邮件、通过 webhook 接收 SendGrid 分析数据以及将分析数据加载到 Google BigQuery 中进行分析。

目标

  • 创建一个 SendGrid 帐号。
  • 编写和部署两个 HTTP Cloud Functions 函数
  • 编写和部署一个 Cloud Functions 后台函数
  • 通过 SendGrid 从部署的函数发送电子邮件。
  • 通过 webhook 接收来自 SendGrid 的分析数据。
  • 将 SendGrid 分析数据加载到 BigQuery 中进行分析。

费用

本教程会使用 Cloud Platform 的如下计费组件:

  • Google Cloud Functions
  • Google BigQuery
  • Google Cloud Storage

您可使用价格计算器根据您的预计使用情况来估算费用。

Cloud Platform 新用户可能有资格申请免费试用

准备工作

  1. 登录您的 Google 帐号。

    如果您还没有 Google 帐号,请注册一个新帐号

  2. 在 Cloud Console 的项目选择器页面上,选择或创建 Cloud 项目。

    转到项目选择器页面

  3. 确保您的 Google Cloud 项目已启用结算功能。 了解如何确认您的项目已启用结算功能

  4. 启用 Cloud Functions, Cloud Storage, and Google BigQuery API。

    启用 API

  5. 安装并初始化 Cloud SDK
  6. 更新 gcloud 组件:
    gcloud components update
  7. 准备开发环境:

直观呈现数据流

SendGrid 教程应用中的数据流涉及以下几个步骤:

  1. 通过 SendGrid 发送电子邮件(只需通过 HTTP 触发一个 Cloud Functions 函数即可)。
  2. SendGrid 通过 HTTP 将分析数据发送给另一个 Cloud Functions 函数。
  3. 系统将分析数据以换行符分隔的 JSON 格式保存到 Cloud Storage 中。
  4. 新的 JSON 文件触发第三个 Cloud Functions 函数,然后该函数将 JSON 文件加入队列,以便加载到 BigQuery 中进行数据分析。

您可以借助下图直观地了解上述步骤:

准备应用

  1. 创建一个 Sendgrid 帐号。您可以通过 SendGrid 网站手动执行此操作,也可以使用 Google Cloud Launcher(该服务将为您创建一个帐号,并集成结算功能)。

    请参阅使用 Cloud Launcher 创建 SendGrid 帐号

  2. 创建 SendGrid API 密钥:

    1. 访问 https://app.sendgrid.com 并登录您的 SendGrid 帐号。
    2. 转至 Settings > API Keys
    3. 创建一个拥有全部权限的新 API 密钥。
    4. 当 API 密钥显示时复制该密钥(您只会看到一次该密钥,请确保将其粘贴到某个位置,以便在本教程结束时使用该密钥来调用您的 Cloud Functions 函数)。
  3. 创建 SendGrid 事件通知:

    1. 访问 https://app.sendgrid.com 并登录您的 SendGrid 帐号。
    2. 转至 Settings > Mail Settings
    3. 打开 Event Notification 标签。
    4. 点击 Edit,然后将以下内容输入 HTTP POST URL 字段中:

      Node.js 6(已弃用)

      https://YOUR_USERNAME:YOUR_PASSWORD@YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/sendgridWebhook

      其中

      • YOUR_USERNAMEYOUR_PASSWORD 是您选择的用户名和密码。在后续步骤中,您需要将这两个变量输入配置文件中。
      • YOUR_REGION 是将要部署函数的区域。当您的函数完成部署时,该信息会显示在您的终端上。
      • YOUR_PROJECT_ID 是您的 Cloud 项目 ID。当您的函数完成部署后,该信息会显示在您的终端。
    5. 选择 All 以将所有操作报告给您的 Cloud Functions 函数。

    6. 最后,将事件通知设置为 On

  4. 使用 BigQuery bq 命令行工具创建一个 BigQuery 数据集:

    bq mk YOUR_DATASET_NAME
    

    其中,YOUR_DATASET_NAME 是新的 BigQuery 数据集的名称。您还可以通过 BigQuery 控制台创建数据集

  5. 创建一个 Cloud Storage 存储分区以保存 JSON 文件,其中,YOUR_EVENT_BUCKET_NAME 是全局唯一的存储分区名称:

    gsutil mb gs://YOUR_EVENT_BUCKET_NAME
    
  6. 将示例应用代码库克隆到本地机器:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

  7. 切换到包含 Cloud Functions 函数示例代码的目录:

    Node.js 6(已弃用)

    cd nodejs-docs-samples/functions/sendgrid/

  8. 配置应用:

    Node.js 6(已弃用)

    使用 config.default.json 文件作为模板,在 sendgrid 目录中创建一个 config.json 文件,其中包含以下内容:
    {
      "EVENT_BUCKET": "YOUR_EVENT_BUCKET_NAME",
      "DATASET": "YOUR_DATASET_NAME",
      "TABLE": "events",
      "USERNAME": "YOUR_USERNAME",
      "PASSWORD": "YOUR_PASSWORD"
    }
    • YOUR_EVENT_BUCKET_NAME 替换为用于保存 JSON 文件的 Cloud Storage 存储分区的名称。
    • YOUR_DATASET_NAME 替换为您的 BigQuery 数据集名称。
    • YOUR_USERNAME 替换为您选择的用户名,该用户名将用于验证来自 SendGrid 的数据。
    • YOUR_PASSWORD 替换为您选择的密码,该密码将用于验证来自 SendGrid 的数据。

了解代码

导入依赖项

应用必须导入多个依赖项才能与 Google Cloud Platform 服务进行通信:

Node.js 6(已弃用)

const sendgrid = require('sendgrid');
const config = require('./config.json');
const uuid = require('uuid');

// Get a reference to the Cloud Storage component
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
// Get a reference to the BigQuery component
const {BigQuery} = require('@google-cloud/bigquery');
const bigquery = new BigQuery();

发送电子邮件

以下函数会创建一个用于发送电子邮件的 SendGrid 客户端:

Node.js 6(已弃用)

/**
 * Returns a configured SendGrid client.
 *
 * @param {string} key Your SendGrid API key.
 * @returns {object} SendGrid client.
 */
const getClient = (key) => {
  if (!key) {
    const error = new Error(
      'SendGrid API key not provided. Make sure you have a "sg_key" property in your request querystring'
    );
    error.code = 401;
    throw error;
  }

  // Using SendGrid's Node.js Library https://github.com/sendgrid/sendgrid-nodejs
  return sendgrid(key);
};

以下函数使用 SendGrid 客户端发送电子邮件:

Node.js 6(已弃用)

/**
 * Send an email using SendGrid.
 *
 * Trigger this function by making a POST request with a payload to:
 * https://[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendEmail?sg_key=[YOUR_API_KEY]
 *
 * @example
 * curl -X POST "https://us-central1.your-project-id.cloudfunctions.net/sendEmail?sg_key=your_api_key" --data '{"to":"bob@email.com","from":"alice@email.com","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"
 *
 * @param {object} req Cloud Function request context.
 * @param {object} req.query The parsed querystring.
 * @param {string} req.query.sg_key Your SendGrid API key.
 * @param {object} req.body The request payload.
 * @param {string} req.body.to Email address of the recipient.
 * @param {string} req.body.from Email address of the sender.
 * @param {string} req.body.subject Email subject line.
 * @param {string} req.body.body Body of the email subject line.
 * @param {object} res Cloud Function response context.
 */
exports.sendgridEmail = async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }

    // Get a SendGrid client
    const client = getClient(req.query.sg_key);

    // Build the SendGrid request to send email
    const request = client.emptyRequest({
      method: 'POST',
      path: '/v3/mail/send',
      body: getPayload(req.body),
    });

    // Make the request to SendGrid's API
    console.log(`Sending email to: ${req.body.to}`);
    const response = await client.API(request);

    if (response.statusCode < 200 || response.statusCode >= 400) {
      const error = Error(response.body);
      error.code = response.statusCode;
      throw error;
    }

    console.log(`Email sent to: ${req.body.to}`);

    // Forward the response back to the requester
    res.status(response.statusCode);
    if (response.headers['content-type']) {
      res.set('content-type', response.headers['content-type']);
    }
    if (response.headers['content-length']) {
      res.set('content-length', response.headers['content-length']);
    }
    if (response.body) {
      res.send(response.body);
    } else {
      res.end();
    }

    return Promise.resolve();
  } catch (err) {
    console.error(err);
    const code =
      err.code || (err.response ? err.response.statusCode : 500) || 500;
    res.status(code).send(err);
    return Promise.reject(err);
  }
};

接收分析数据

以下函数通过检查已配置的用户名和密码来对传入的 SendGrid 请求进行身份验证:

Node.js 6(已弃用)

/**
 * Verify that the webhook request came from sendgrid.
 *
 * @param {string} authorization The authorization header of the request, e.g. "Basic ZmdvOhJhcg=="
 */
const verifyWebhook = (authorization) => {
  const basicAuth = Buffer.from(
    authorization.replace('Basic ', ''),
    'base64'
  ).toString();
  const parts = basicAuth.split(':');
  if (parts[0] !== config.USERNAME || parts[1] !== config.PASSWORD) {
    const error = new Error('Invalid credentials');
    error.code = 401;
    throw error;
  }
};

以下函数会接收来自 SendGrid 的分析数据,并将数据以换行符分隔的 JSON 格式保存到 Cloud Storage 中:

Node.js 6(已弃用)

/**
 * Receive a webhook from SendGrid.
 *
 * See https://sendgrid.com/docs/API_Reference/Webhooks/event.html
 *
 * @param {object} req Cloud Function request context.
 * @param {object} res Cloud Function response context.
 */
exports.sendgridWebhook = async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }

    verifyWebhook(req.get('authorization') || '');

    const events = req.body || [];

    // Make sure property names in the data meet BigQuery standards
    fixNames(events);

    // Generate newline-delimited JSON
    // See https://cloud.google.com/bigquery/data-formats#json_format
    const json = events.map((event) => JSON.stringify(event)).join('\n');

    // Upload a new file to Cloud Storage if we have events to save
    if (json.length) {
      const bucketName = config.EVENT_BUCKET;
      const unixTimestamp = new Date().getTime() * 1000;
      const filename = `${unixTimestamp}-${uuid.v4()}.json`;
      const file = storage.bucket(bucketName).file(filename);

      console.log(`Saving events to ${filename} in bucket ${bucketName}`);

      await file.save(json);
      console.log(`JSON written to ${filename}`);
    }
    res.status(200).end();
  } catch (err) {
    console.error(err);
    res.status(err.code || 500).send(err);
    return Promise.reject(err);
  }
};

将数据导入到 BigQuery 中

最后,以下函数会将以换行符分隔的 JSON 数据导入到 BigQuery 中:

Node.js 6(已弃用)

/**
 * Cloud Function triggered by Cloud Storage when a file is uploaded.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data A Cloud Storage file object.
 * @param {string} event.data.bucket Name of the Cloud Storage bucket.
 * @param {string} event.data.name Name of the file.
 * @param {string} [event.data.timeDeleted] Time the file was deleted if this is a deletion event.
 * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
 */
exports.sendgridLoad = async (event) => {
  const file = event.data;

  if (file.resourceState === 'not_exists') {
    // This was a deletion event, we don't want to process this
    return;
  }

  try {
    if (!file.bucket) {
      throw new Error(
        'Bucket not provided. Make sure you have a "bucket" property in your request'
      );
    } else if (!file.name) {
      throw new Error(
        'Filename not provided. Make sure you have a "name" property in your request'
      );
    }

    const [table] = await getTable();

    const fileObj = storage.bucket(file.bucket).file(file.name);
    console.log(`Starting job for ${file.name}`);
    const metadata = {
      autodetect: true,
      sourceFormat: 'NEWLINE_DELIMITED_JSON',
    };
    const [job] = await table.import(fileObj, metadata);

    await job.promise();
    console.log(`Job complete for ${file.name}`);
  } catch (err) {
    console.log(`Job failed for ${file.name}`);
    return Promise.reject(err);
  }
};

部署函数

  1. 要使用 HTTP 触发器部署发送电子邮件的函数,请在 sendgrid 目录中运行以下命令:

    Node.js 6(已弃用)

    /**
     * Cloud Function triggered by Cloud Storage when a file is uploaded.
     *
     * @param {object} event The Cloud Functions event.
     * @param {object} event.data A Cloud Storage file object.
     * @param {string} event.data.bucket Name of the Cloud Storage bucket.
     * @param {string} event.data.name Name of the file.
     * @param {string} [event.data.timeDeleted] Time the file was deleted if this is a deletion event.
     * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
     */
    exports.sendgridLoad = async (event) => {
      const file = event.data;
    
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }
    
      try {
        if (!file.bucket) {
          throw new Error(
            'Bucket not provided. Make sure you have a "bucket" property in your request'
          );
        } else if (!file.name) {
          throw new Error(
            'Filename not provided. Make sure you have a "name" property in your request'
          );
        }
    
        const [table] = await getTable();
    
        const fileObj = storage.bucket(file.bucket).file(file.name);
        console.log(`Starting job for ${file.name}`);
        const metadata = {
          autodetect: true,
          sourceFormat: 'NEWLINE_DELIMITED_JSON',
        };
        const [job] = await table.import(fileObj, metadata);
    
        await job.promise();
        console.log(`Job complete for ${file.name}`);
      } catch (err) {
        console.log(`Job failed for ${file.name}`);
        return Promise.reject(err);
      }
    };

  2. 要使用 HTTP 触发器部署 SendGrid webhook 函数,请在 sendgrid 目录中运行以下命令:

    Node.js 6(已弃用)

    /**
     * Cloud Function triggered by Cloud Storage when a file is uploaded.
     *
     * @param {object} event The Cloud Functions event.
     * @param {object} event.data A Cloud Storage file object.
     * @param {string} event.data.bucket Name of the Cloud Storage bucket.
     * @param {string} event.data.name Name of the file.
     * @param {string} [event.data.timeDeleted] Time the file was deleted if this is a deletion event.
     * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
     */
    exports.sendgridLoad = async (event) => {
      const file = event.data;
    
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }
    
      try {
        if (!file.bucket) {
          throw new Error(
            'Bucket not provided. Make sure you have a "bucket" property in your request'
          );
        } else if (!file.name) {
          throw new Error(
            'Filename not provided. Make sure you have a "name" property in your request'
          );
        }
    
        const [table] = await getTable();
    
        const fileObj = storage.bucket(file.bucket).file(file.name);
        console.log(`Starting job for ${file.name}`);
        const metadata = {
          autodetect: true,
          sourceFormat: 'NEWLINE_DELIMITED_JSON',
        };
        const [job] = await table.import(fileObj, metadata);
    
        await job.promise();
        console.log(`Job complete for ${file.name}`);
      } catch (err) {
        console.log(`Job failed for ${file.name}`);
        return Promise.reject(err);
      }
    };

  3. 要使用 Cloud Storage 触发器部署将数据加载到 BigQuery 的函数,请在 sendgrid 目录中运行以下命令:

    Node.js 6(已弃用)

    /**
     * Cloud Function triggered by Cloud Storage when a file is uploaded.
     *
     * @param {object} event The Cloud Functions event.
     * @param {object} event.data A Cloud Storage file object.
     * @param {string} event.data.bucket Name of the Cloud Storage bucket.
     * @param {string} event.data.name Name of the file.
     * @param {string} [event.data.timeDeleted] Time the file was deleted if this is a deletion event.
     * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
     */
    exports.sendgridLoad = async (event) => {
      const file = event.data;
    
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }
    
      try {
        if (!file.bucket) {
          throw new Error(
            'Bucket not provided. Make sure you have a "bucket" property in your request'
          );
        } else if (!file.name) {
          throw new Error(
            'Filename not provided. Make sure you have a "name" property in your request'
          );
        }
    
        const [table] = await getTable();
    
        const fileObj = storage.bucket(file.bucket).file(file.name);
        console.log(`Starting job for ${file.name}`);
        const metadata = {
          autodetect: true,
          sourceFormat: 'NEWLINE_DELIMITED_JSON',
        };
        const [job] = await table.import(fileObj, metadata);
    
        await job.promise();
        console.log(`Job complete for ${file.name}`);
      } catch (err) {
        console.log(`Job failed for ${file.name}`);
        return Promise.reject(err);
      }
    };

    其中,YOUR_EVENT_BUCKET_NAME 是用于保存 JSON 文件的 Cloud Storage 存储分区的名称。

发送电子邮件

  1. 使用以下命令发送电子邮件:

    Node.js 6(已弃用)

    curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/sendgridEmail?sg_key=YOUR_SENDGRID_KEY" --data '{"to":"YOUR_RECIPIENT_ADDR","from":"YOUR_SENDER_ADDR","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"

    其中

    • YOUR_REGION 是部署函数的区域。当您的函数完成部署后,该信息会显示在您的终端。
    • YOUR_PROJECT_ID 是您的 Cloud 项目 ID。当您的函数完成部署后,该信息会显示在您的终端。
    • YOUR_SENDGRID_KEY 是您的 SendGrid API 密钥。
    • YOUR_RECIPIENT_ADDR 是收件人的电子邮件地址。
    • YOUR_SENDER_ADDR 是您的 SendGrid 帐号的电子邮件地址。
  2. 查看日志以确保执行已完成:

    gcloud functions logs read --limit 100
    
  3. 您可以在由配置文件中的 EVENT_BUCKET 值指定的 Cloud Storage 存储分区中查看保存的 JSON 文件。

  4. 您可以通过访问以下网址查看 BigQuery 中已导入的分析数据:

    https://bigquery.cloud.google.com/table/YOUR_PROJECT_ID:YOUR_DATASET_NAME.events
    

    其中

    • YOUR_PROJECT_ID 是您的 Google Cloud 项目 ID。
    • YOUR_DATASET_NAME 是您之前配置的 BigQuery 数据集名称。

清理

为避免因本教程中使用的资源而导致您的 Google Cloud Platform 帐号产生费用,请执行以下操作:

删除项目

为了避免产生费用,最简单的方法是删除您为本教程创建的项目。

要删除项目,请执行以下操作:

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”页面

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除 Cloud Functions 函数

删除 Cloud Functions 函数不会移除存储在 Cloud Storage 或 BigQuery 中的任何资源。

要删除 Cloud Functions 函数,请运行以下命令:

gcloud functions delete NAME_OF_FUNCTION

其中,NAME_OF_FUNCTION 是要删除的函数的名称。

您也可以通过 Google Cloud Console 删除 Cloud Functions 函数。