6
votes

I'm developing a Firebase Function, which is triggered when a new order is added to the Realtime Database. The first thing it does is to creat a pdf and pipe it to a google cloud storage bucket.

On the .on("finish") event of the bucket stream, the next function gets started, which should send the piped pdf via email to the customer.

Everything seems to work, at least a bit.

First I had the problem, that the attached pdf always was empty. (Not just blank. I also opened it in notepad++ and it really was all empty). When I checked the doc and bucketFileSream vars inside the bucketFileStream.on("finished") function both had a length of 0. A check of the doc var directly after doc.end showed a length of somewhat 612.

I then changed the flow, that in the sendOrderEmail function I also open a new Read Stream from the newly created File in the bucket.

Now I get at least some stuff of the PDF in the attachement, but never the whole content.

When I check the PDF uploaded to the bucket, it looks like it should.

I googled alot and found some answers that were also targeting this topic, but as also seen in comments on these questions, they were not completly helpful.

I also checked with the nodemailer documentation how to pass the attachement correctly and implemented it as documented. But no success.

I think that the mail gets sent before the Read Stream has finished.

Here the Package Versions I use:

  • "@google-cloud/storage": "1.5.2"
  • "@types/pdfkit": "^0.7.35",
  • "firebase-admin": "5.8.0",
  • "firebase-functions": "^0.7.3"
  • "nodemailer": "4.4.1",

Can anyone tell me what I'm doing wrong or provide a working example, which uses current package versions, for this usecase?

Here is the code which drives me crazy...

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const nodemailer = require("nodemailer");
const pdfkit = require("pdfkit");
const storage = require("@google-cloud/storage")({projectId: `${PROJECT_ID}`})

const mailTransport = nodemailer.createTransport({
  host: "smtp.office365.com",
  port: 587,
  secureConnection: false,
  auth: {
    user: "userName",
    pass: "userPassword"
 },
 tls: {
   ciphers: "SSLv3",
 }
});

exports.added = function(event) {
  const order = event.data.val();
  const userId = event.params.userId;

  // Load User Data by userId
  return admin
       .database()
       .ref("/users/" +userId)
       .once("value")
       .then(function (snapshot) {
               return generateOrderPDF(snapshot.val(), userId);
       });
};

function generateOrderPDF(user, userId) {
  const doc = new pdfkit();
  const bucket = storage.bucket(functions.config().bucket);
  const filename =  `/${userId}/test-` + Date.now() + ".pdf";
  const file = bucket.file(filename);
  const bucketFileStream = file.createWriteStream();

  // Pipe its output to the bucket
  doc.pipe(bucketFileStream);

 // Do creation Stuff....

  doc.end();

  bucketFileStream.on("finish", function () {
    return sendOrderEmail(user, filename);
  });

  bucketFileStream.on("error", function(err) {
    console.error(err);
  });
}

function sendOrderEmail(user, filename) {
  const email = user.email;
  const firstname = user.firstName;
  const mailOptions = {
    from: "[email protected]",
    to: email,
    subject: "Order"
  };

  const bucket = storage.bucket(functions.config().bucket);
  const file = bucket.file(filename);

  mailOptions.html = mailTemplate;
  mailOptions.attachments =  [{
    filename: "test.pdf",
    content: file.createReadStream()
  }];

  return mailTransport.sendMail(mailOptions).then(() => {
    console.log("New order email sent to:", email);
  }).catch(error => {
    console.error(error);
  });
}
2
Does the PDF look like it should in storage?Jen Person
Yes. The PDF in the bucket looks fine. I've edited the question to add this additional information.LukeFilewalker
I invested much time till now. I figured out, that the function completes too early with the above code. If I wrap the bucketFileStream Events into a promise, the execution order would be right, but the attached pdf is still not complete. I also tested with not pipe the output and also download the pdf from storage to tmpDir and then attach it. It is always the same uncomplete pdf attached. Even with a timeout of 10 seconds before send it still fails.LukeFilewalker

2 Answers

6
votes

The problem in my appraoch was inside the pdfkit library and not inside nodemailer or firebase. The lines below seem to trigger the end event. So the pdf got sent after these lines. After out commenting them everything worked as it should. It was not that finish was never reached like Hari mentioned.

/*  doc.lineCap("underline")
    .moveTo(72, 321)
    .lineTo(570, 321)
    .stroke();*/

After finishing the MVP I will take a root cause analysis and post the final answer as comment below this answer.

This is a working sample of Source-Code for this UseCase. It also ensures, that the firebase function won't finish before all work is done. That is handled by wrapping the event driven doc.on() function into a promise, that is resolved when doc.on("end") is called.

exports.added = function(event) {
  const order = event.data.val();
  const userId = event.params.userId;

  // Load User Data by userId
  return admin.database().ref("/users/" + userId).once("value").then(function (snapshot) {
    return generatePDF(snapshot.val(), userId);
  });
};

function generatePDF(user, userId) {
  const doc = new pdfkit();
  const bucket = admin.storage().bucket(functions.config().moost.orderbucket);
  const filename =  "/${userId}/attachement.pdf";
  const file = bucket.file(filename);
  const bucketFileStream = file.createWriteStream();
  var buffers = [];
  let p = new Promise((resolve, reject) => {
    doc.on("end", function() {
      resolve(buffers);
    });
    doc.on("error", function () {
      reject();
    });
  });

  doc.pipe(bucketFileStream);
  doc.on('data', buffers.push.bind(buffers));

  //Add Document Text and stuff

  doc.end();

  return p.then(function(buffers) {
     return sendMail(buffers);
  });
}

function sendMail(buffers) {
  const pdfData = Buffer.concat(buffers);
  const mailOptions = {
    from: "FromName <[email protected]>",
    to: "[email protected]",
    subject: "Subject",
    html: mailTemplate,
    attachments: [{
      filename: 'attachment.pdf',
      content: pdfData
    }]
  };

  return mailTransport.sendMail(mailOptions).then(() => {
    console.log("New email sent to:", "[email protected]");
  }).catch(error => {
    console.error(error);
  });
}
4
votes

The main problem in your code is that the stream.on('finish') never completes. I've also encountered the same issue.

Instead of streaming, convert the pdf into buffer and send the same as attachment.

The following works fine for me,

 const doc = new pdfkit()
 const filename =  '/${userId}/test-' + Date.now() + ".pdf"
 const file = bucket.file(filename);
 const bucketFileStream = file.createWriteStream();
 doc.pipe(bucketFileStream);
 doc.end();
 var buffers = []
 doc.on('data', buffers.push.bind(buffers));
 doc.on('end',function(){
     let pdfData = Buffer.concat(buffers);
     '<<nodemailer stuffs goes here>
     'attach the doc as content
 });