Search code examples
node.jsfirebaseasync-awaitgoogle-cloud-functionsnodemailer

How to reduce email sending time (using nodemailer and firebase)?


We have written code that sends emails to a user and their contacts, when a new node is added to a specific path in Firebase realtime database.

The average time to send the emails is 4 minutes. We think the problem is due to awaiting for some needed promises. We would like to get the run time down. Do you have any advice? Thanks in advance!

This is our code:

const functions = require("firebase-functions");
const nodemailer = require('nodemailer');
require('dotenv').config()

//for fire store
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

const { SENDER_EMAIL, SENDER_PASSWORD } = process.env;

exports.sendEmails = functions.database.ref("/devices/{device_ID}/history/{alert_ID}")
  .onWrite(
    (snapshot, context) => { 

      sendMail(snapshot, context);

      return true;
    }
  );

  async function sendMail(snapshot, context){

    const { before, after } = snapshot;

    // new alert created
    if (before.val() == null) {

      console.log('DEBUG:: NEW ALERT');

      // get owners uID from device ID
      const deviceRef = db.collection('deviceToUid').doc(context.params.device_ID);
      const uidDoc = await deviceRef.get();

      if(!uidDoc.exists){
        functions.logger.info("No such document!");
        return;
      }

      // get users email from uID
      const userRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('user-info');

      // get users contact
      const contactRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('contacts');

      const [userInfo, contactList] =  await Promise.all([userRef.get(), contactRef.get()]);

      if(userInfo.empty){
        functions.logger.info("No such collection!");
        return;
      }

      const email = userInfo.docs[0].id; // owners email

      let contacts = []; // initialize contact list

      contactList.forEach(
        (doc) => {
          if(doc.data().confirmed){
            contacts.push(doc.id);
          }
        }
      )      

      const mailTransport = nodemailer.createTransport({
        service: 'gmail',
        auth: {
          user: SENDER_EMAIL,
          pass: SENDER_PASSWORD,
        },
      });

      const mailOptions = {
        from: 'ALERT <noreply@firebase.com>',
        to: email,
        bcc: contacts,
        subject: `...Motion detected`,
        html: `<p dir=ltr>New Alert...</p>`
        
      };

      mailTransport.sendMail(mailOptions, function (error, info) {
        if (error) {
          console.log(error);
        } else {
          console.log('Email sent: ' + info.response);
        }
      });
    }
  }

I'd also recommend learning a bit about list comprehensions, as this:

  let contacts = []; // initialize contact list

  contactList.forEach(
    (doc) => {
      if(doc.data().confirmed){
        contacts.push(doc.id);
      }
    }
  )

Can be reduced to a more concise:

let contacts = contactList.docs
                          .filter((doc) => doc.data().confirmed)
                          .map((doc) => doc.id);

Solution

  • You were getting pretty close, but were missing an await in the top-level function, and one inside sendMail for the call to mailTransport.sendMail.

    I think this should be it:

    exports.sendEmails = functions.database.ref("/devices/{device_ID}/history/{alert_ID}")
      .onWrite(
        async (snapshot, context) => { 
          await sendMail(snapshot, context);
          return true;
        }
      );
    
      async function sendMail(snapshot, context){
    
        const { before, after } = snapshot;
    
        // new alert created
        if (before.val() == null) {
    
          console.log('DEBUG:: NEW ALERT');
    
          // get owners uID from device ID
          const deviceRef = db.collection('deviceToUid').doc(context.params.device_ID);
          const uidDoc = await deviceRef.get();
    
          if(!uidDoc.exists){
            functions.logger.info("No such document!");
            return;
          }
    
          // get users email from uID
          const userRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('user-info');
    
          // get users contact
          const contactRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('contacts');
    
          const [userInfo, contactList] =  await Promise.all([userRef.get(), contactRef.get()]);
    
          if(userInfo.empty){
            functions.logger.info("No such collection!");
            return;
          }
    
          const email = userInfo.docs[0].id; // owners email
    
          let contacts = []; // initialize contact list
    
          contactList.forEach(
            (doc) => {
              if(doc.data().confirmed){
                contacts.push(doc.id);
              }
            }
          )      
    
          const mailTransport = nodemailer.createTransport({
            service: 'gmail',
            auth: {
              user: SENDER_EMAIL,
              pass: SENDER_PASSWORD,
            },
          });
    
          const mailOptions = {
            from: 'ALERT <noreply@firebase.com>',
            to: email,
            bcc: contacts,
            subject: `...Motion detected`,
            html: `<p dir=ltr>New Alert...</p>`
            
          };
    
          await mailTransport.sendMail(mailOptions, function (error, info) {
            if (error) {
              console.log(error);
            } else {
              console.log('Email sent: ' + info.response);
            }
          });
          return true;
        }
      }
    

    Since you were not using await in the top-level call, the Cloud Functions contains will/may shut down the container before the asynchronous calls have completed. For more on this, see the documentation on sync, async and promises - and how Cloud Functions are terminated.