如何从 Firebase Storage ObjectMetadata 通过电子邮件发送文件

How to send file by email from Firebase Storage ObjectMetadata

我正在开发一个触发器函数,用于侦听指定存储桶中的新对象。我想要的是使用nodemailer发送邮件返回的对象。

const transport = nodemailer.createTransport({
  host: "smtp.gmail.com",
  port: 465,
  secure: true,
  auth: {
    user: "xxxxxxxxxx@gmail.com",
    pass: "xxxxxxxxxxxxxxx"
  }
});

exports.sendConfirmationEmail = functions.storage.bucket('bucket-name').object().onFinalize(async (object) => {
  const orderID = object.name.slice(0, -4);
  admin.database().ref('/pedidos/' + orderID).once('value', (snapshot) => {
    return sendEmail(snapshot.val().customer, snapshot.val().email, snapshot.val().number, /*FILE*/);
  });
});

function sendEmail(user, email, order, file){
  console.log("Sending Email...");
  return transport.sendMail({
      from: "XXXXXX <xxxxxxxxxx@gmail.com>",
      to: email,
      subject: "Confirmación pedido " + order,
      html: `
            <h1> Estiamdo ${user}, </h1>
            <p> Hemos recibido su pedido correctamente. Le mantendremos infromado de su estado. </p>
            <p> Gracias por confiar en nosotros </p>
          `,
      attachment: file
    })
    .then(r => r)
    .catch(e => {
      console.log("An error has ocurred " + e);
    });
  }
}

有人可以帮忙吗?

首先让我们修复您的 Cloud Functions 的主要部分。为您的函数编写代码时,一般来说,不要使用 Admin SDK 的回调 API。

admin.database().ref('/pedidos/' + orderID).once('value', (snapshot) => { /* ... */ });

应该是

admin.database().ref('/pedidos/' + orderID).once('value')
  .then((snapshot) => { /* ... */ });

const snapshot = await admin.database().ref('/pedidos/' + orderID);

确保您 returnawait 事件处理程序中的任何承诺也很重要,否则您的代码可能会随时终止,从而导致意外错误。

由于您只发送 PDF 文档,我们将忽略所有非 PDF 文件。

exports.sendConfirmationEmail = functions.storage.bucket('bucket-name').object().onFinalize(async (object) => {
  if (object.contentType !== "application/pdf")
    return; // ignore non-pdfs
  
  const orderID = object.name.slice(0, -4);

  // ↓↓ this return is needed
  return admin.database().ref('/pedidos/' + orderID).once('value')
    .then((snapshot) => {
      return sendEmail(snapshot.val().customer, snapshot.val().email, snapshot.val().number, /*FILE*/);
    });
});

接下来,我们继续您的 sendEmail 函数。在您当前的 sendEmail 函数中,您错误地使用了 attachment 而不是 attachments。您还可以删除这些只会引入问题的行:

.then(r => r) // doesn't do anything
.catch(e => { // logs an error, but incorrectly prevents it being handled by .catch() elsewhere
  console.log("An error has ocurred " + e);
});

这允许我们将 sendEmail 重新定义为:

function sendEmail (user, email, order, attachments = undefined) {
  return transport.sendMail({
    from: "XXXXXX <xxxxxxxxxx@gmail.com>",
    to: email,
    subject: "Confirmación pedido " + order,
    html: `
          <h1> Estiamdo ${user}, </h1>
          <p> Hemos recibido su pedido correctamente. Le mantendremos infromado de su estado. </p>
          <p> Gracias por confiar en nosotros </p>
          `,
    attachments
  });
}

接下来,让我们查看 attachments property 的文档:

attachments option in the message object that contains an array of attachment objects.

Attachment object consists of the following properties:

  • filename - filename to be reported as the name of the attached file. Use of unicode is allowed.
  • content - String, Buffer or a Stream contents for the attachment
  • path - path to the file if you want to stream the file instead of including it (better for larger attachments)
  • href – an URL to the file (data uris are allowed as well)
  • httpHeaders - optional HTTP headers to pass on with the href request, eg. {authorization: "bearer ..."}
  • contentType - optional content type for the attachment, if not set will be derived from the filename property
  • contentDisposition - optional content disposition type for the attachment, defaults to ‘attachment’
  • cid - optional content id for using inline images in HTML message source
  • encoding - If set and content is string, then encodes the content to a Buffer using the specified encoding. Example values: ‘base64’, ‘hex’, ‘binary’ etc. Useful if you want to use binary attachments in a JSON formatted email object.
  • headers - custom headers for the attachment node. Same usage as with message headers
  • raw - is an optional special value that overrides entire contents of current mime node including mime headers. Useful if you want to prepare node contents yourself

现在我们知道我们可以为每个附件对象使用什么,我们需要将该列表与我们可以从 object parameter passed into the Storage Event Cloud Function 事件处理程序中提取的内容进行比较。主要属性包括:

const fileBucket = object.bucket; // The Storage bucket that contains the file.
const filePath = object.name; // File path in the bucket.
const contentType = object.contentType; // File content type.
const metageneration = object.metageneration; // Number of times metadata has been generated. New objects have a value of 1.

因此,对于我们的附件,我们要提供 filenamecontentcontentDispositioncontentType。因为我们发送的 Cloud Storage 对象不在磁盘或内存中,所以我们将通过将 Stream 作为 content 属性 我们的附件对象。这导致:

const bucket = admin.storage().bucket(object.bucket);
const remoteFile = bucket.file(object.name);

const attachment = {
  filename: `order-${orderID}.pdf`,       // the attachment will be called `order-<ID>.pdf`
  content: remoteFile.createReadStream(), // stream data from Cloud Storage
  contentType: object.contentType,        // use appropriate content type
  contentDisposition: "attachment",       // this file is a downloadable attachment
};

我们现在可以将它们汇总在一起并使用 async/await 语法进行清理:

const transport = nodemailer.createTransport({ /* ... */ });

exports.sendConfirmationEmail = functions.storage.bucket('bucket-name').object().onFinalize(async (object) => {
  if (object.contentType !== "application/pdf") {
    console.log("Content-Type was not application/pdf. Ignoring.");
    return; // ignore non-pdfs
  }

  // if (object.metageneration > 1) {
  //   console.log("Metageneration was greater than 1. Ignoring.");
  //   return; // ignore rewritten files
  // }
  
  try {
    const orderID = object.name.slice(0, -4);

    const orderSnapshot = await admin.database()
      .ref(`/pedidos/${orderID}`)
      .once('value');

    if (!orderSnapshot.exists) {
      console.error(`Order #${orderID} document not found`);
      return;
    }

    const { customer, email, number } = orderSnapshot.val();

    // prepare attachment
    const bucket = admin.storage().bucket(object.bucket);
    const remoteFile = bucket.file(object.name);

    const attachment = {
      filename: `order-${orderID}.pdf`,       // override name of the PDF
      content: remoteFile.createReadStream(), // stream data from Cloud Storage
      contentType: object.contentType,        // use appropriate content type
      contentDisposition: "attachment",       // this file is a downloadable attachment
    };

    console.log("Sending confirmation email...");
    await sendEmail(customer, email, number, [ attachment ]);

    console.log(`Email confirmation was sent successfully for Order #${orderID}`);
  } catch (error) {
    console.error("Unexpected error: ", error);
  }
});

function sendEmail (user, email, order, attachments = undefined) {
  return transport.sendMail({
    from: "XXXXXX <xxxxxxxxxx@gmail.com>",
    to: email,
    subject: "Confirmación pedido " + order,
    html: `
          <h1> Estiamdo ${user}, </h1>
          <p> Hemos recibido su pedido correctamente. Le mantendremos infromado de su estado. </p>
          <p> Gracias por confiar en nosotros </p>
          `,
    attachments
  });
}

注意:metageneration大于1时,您应该决定是否发送电子邮件。

附录: 我强烈建议使用 functions.config() 来存储诸如 nodemailer 的 username/password 连击之类的东西,而不是将它们写入您的代码中。