1
votes

Made a cloud function which will send email of invitation and upon successful send it will update a map object field but for some reason it is only working for the last object not all of them

Function Code

exports.sendInvitationEmailForMembers = functions.firestore
  .document("/organization-members/{OMid}")
  .onWrite(async (snap, context) => {
    if (snap.after.data() === undefined) {
      return null;
    }
    let authData = nodeMailer.createTransport({
      host: "smtp.gmail.com",
      port: 465,
      secure: true,
      auth: {
        user: SENDER_EMAIL,
        pass: SENDER_PASSWORD,
      },
    });
    // Exit when the data is deleted.
    const id = context.params.OMid;
    const afterData = snap.after.data();
    const db = admin.firestore();
    for (var prop in afterData) {
      if (afterData[prop].status === "invited") {
        const email = afterData[prop].email;
        const name = afterData[prop].fullName;

        authData
          .sendMail({
            from: "[email protected]",
            to: `${email}`,
            subject: "Organization Team Invitation",
            text: `Dear ${name}, you have been invited to join the team of 'organization name', please click on the following link to sign up and join. <br> http://localhost:3000/`,
            html: `Dear ${name}, you have been invited to join the team of 'organization name', please click on the following link to sign up and join. <br> http://localhost:3000/`,
          })
          .then(() => {
            db.collection("organization-members")
              .doc(id)
              .update({ [[prop] + ".status"]: "pending" })
              .then(() => {
                console.log("Successfully updated");
              })
              .catch(() => {
                console.log("Error occured while updating invitation status");
              });
          })
          .catch((err) => console.log(err.message));
      }
    }
  });

Notice that only last object status was changed but it should've changed the whole document objects statuses

enter image description here

1

1 Answers

2
votes

There are a number of small problems with the code as it is. These include:

  • Your onWrite function is self-triggered. Each time you send an email, you currently update the document with the send status for that email - however this means that invites are sent again to those which haven't yet had their status updated. This means that in the worst case scenario, if you had N recipients - each of those users could get sent your invite email up to N times.
  • In a similar vein, if a call to sendMail fails, it will be reattempted every time the document updates. If one particular email were to fail consistently, you may end up in an infinite loop. This can be mitigated by setting a "failed" status.
  • Your GMail authentication details appear to be in your code. Consider using Environment Configurations for this.
  • Your function is connecting to GMail to send the emails, even if there are no emails to be sent.
  • Use console.log and console.error as appropriate so they are shown properly in the Cloud Functions logs. Such as logging why a function ended.
  • The text property of the email should not contain HTML like <br>
exports.sendInvitationEmailForMembers = functions.firestore
  .document("/organization-members/{OMid}")
  .onWrite(async (change, context) => {
    // If deleted, cancel
    if (!change.after.exists) {
      console.log("Document deleted. Cancelling function.");
      return null;
    }
    
    const OMid = context.params.OMid; // keep parameter names consistent.
    const data = change.after.data();
    
    const pendingInvitations = [];
    for (const index in data) {
      if (data[index].status === "invited") {
        // collect needed information
        pendingInvitations.push({
          email: data.email,
          fullName: data.fullName,
          index
        });
        
        // alternatively: pendingInvitations.push({ ...data[index], index });
      }
    }
    
    // If no invites needed, cancel
    if (pendingInvitations.length == 0) {
      console.log("No invitations to be sent. Cancelling function.");
      return null;
    }
    
    // WARNING: Gmail has a sending limit of 500 emails per day.
    // See also: https://support.google.com/mail/answer/81126
    
    // Create transport to send emails
    const transport = nodeMailer.createTransport({
      host: "smtp.gmail.com",
      port: 465,
      secure: true,
      auth: {
        user: SENDER_EMAIL, // Suggestion: Use `functions.config()`, see above
        pass: SENDER_PASSWORD,
      },
    });
    
    // Create an object to store all the status changes
    const pendingDocUpdate = {};
    
    // For each invitation, attempt to send the email, and stash the result in `pendingDocUpdate`.
    // Wait until all emails have been sent.
    await Promise.all(pendingInvitations.map((invitee) => {
      return transport
          .sendMail({
            from: "[email protected]", // shouldn't this be SENDER_EMAIL?
            to: `${invitee.email}`,
            subject: "Organization Team Invitation",
            text: `Dear ${invitee.name}, you have been invited to join the team of 'organization name', please visit this link to sign up and join. http://localhost:3000/`,
            html: `Dear ${invitee.name}, you have been invited to join the team of 'organization name', please click on the following link to sign up and join. <br> http://localhost:3000/`,
          })
          .then(() => {
            console.log(`Sent invite to ${invitee.email}.`);
            pendingDocUpdate[`${invitee.index}.status`] = "pending";
          }, (err) => {
            console.error(`Failed to invite ${invitee.email}.`, err);
            pendingDocUpdate[`${invitee.index}.status`] = "failed"; // save failure, so that emails are not spammed at the invitee
          });
    }));
    
    // Update the document with the changes
    return admin.firestore()
      .doc(`/organization-members/${OMid}`)
      .update(pendingUpdates)
      .then(() => {
        console.log('Applied invitation status changes successfully. Finished.')
      }, (err) => {
        console.error('Error occured while updating invitation statuses.', err);
      });
  });