Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 <[email protected]>',
        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);
like image 397
Sunny Avatar asked Nov 08 '25 11:11

Sunny


1 Answers

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 <[email protected]>',
        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.

like image 120
Frank van Puffelen Avatar answered Nov 10 '25 04:11

Frank van Puffelen