Nodemailer not working in production

Hello all!

I was working on a personal project for the past week and have been testing it locally using Netlify Dev. Now that it was all working, I started testing it in production, but seems like it’s not working as expected for one thing.

To provide some context, I was trying to develop my own static comments solution powered by Netlify Functions. I have added a feature where we could reply to existing comments and the creator of the original comment would be notified by email (if they had provided one while submitting the comment). I’m storing the database on FaunaDB and sending out the emails using Nodemailer and Sendinblue SMTP.

As I have said before, the local tests worked fine, I was receiving emails, the production one however, doesn’t seem to work. What’s strange to me that, it seems as if the Nodemailer is not even trying to send e-mail because in my Sendinblue dashboard, I don’t see any email being sent out (as opposed to my local tests in which I could not only see the email being sent, but also delivered and email opens were tracked too).

My Functions log returns no error, so I’m not sure what’s happening.

Here’s my repo if someone wants to test: https://github.com/Hrishikesh-K/Comments

You’d need some environment variables to test and since I can’t expect anyone to have or create accounts on FaunaDB and Sendinblue to help me, I’m giving out access to test keys which I will delete once the thread is solved.

FAUNADB = fnAEHKkAkmACBfKxAaCfhuNxH8UJg-2OI_NUIJe_
SENDINBLUE_USER = netlify.test@hrishikeshk.ml
SENDINBLUE_PASS = xsmtpsib-42e7ae48396b5f44d1637ea22ef603690a800a62f5b0d0972530f6e1d795c6cb-ND8tXMpsCHxyOKq1
SENDINBLUE_SENDER = "Foo Bar <foo@bar.com>"

Save the above text in a .env file in the root of the project folder (while testing locally), or save it as environment variables in Netlify UI.

Here’s the link to the website: https://comments-test.netlify.app/#296298668511724039

In case it’s needed, the function responsible for sending comments is here: https://github.com/Hrishikesh-K/Comments/blob/main/functions/addComment.js#L93-L105. The code above that is fetching the database entry with the required ID and getting the email address from that.

Any help will be appreciated.

Hey there, @hrishikesh :wave:

Thanks so much for your patience here, and my apologies for the delay. I assure you we haven’t forgotten about this question! I’ve looped in some Support Engineers who will be able to give you some more guidance :slight_smile:

Hey @hrishikesh!
Finally got a chance to dive into this. I forked your repo and added a bunch of logs: https://github.com/kaganjd/Comments. I think I may have found, if not THE culprit, then A culprit. Here’s some output from the function logs (with my email changed):

submittedData: [
  'name=howdy+v3',
  'email=hello%40randomemail.com',
  'parent=null', <------ this seems incorrect/unexpected
  'comment=test'
]
...
commentData: {
  ref: Ref(Collection("commentsTest"), "298340475185660422"),
  ts: 1620778498810000,
  data: {
    comment: '<p>test</p>',
    email: 'hello@randomemail.com',
    parent: 'null', <------ this seems incorrect/unexpected
    name: 'howdy v3'
  }
}

Because parent is null, I believe this code path is never taken: https://github.com/kaganjd/Comments/blob/main/functions/addComment.js#L84

What do you think?

Thank you very much for looking at this, but I believe it’s not the cause. E-mail notifications would be triggered when someone has replied to a previous existing comment, which would mean, the value of parent is not null. The value of parent is the ref of the ‘parent comment’. For testing purposes, you can try keeping it not null, any random +ve integer would do. Basically, my code is checking if the parent could be parsed an an integer. If yes, it means that the comment has a parent. So, I go ahead and check if the email contains @ which is a requirement for any valid email address and if yes, I send the notification.

Gotcha, thanks! Digging back into this and being sure to reply to an existing comment instead of create a fresh comment, I’m seeing that I can log within the if statement, but not after the then statement:

...
        if (parseInt(commentData.data.parent)) {
          // Get the document with parent's ID
          console.log("inside parseInt"); <--- this is logged in the function logs
          commentsDB
            .query(
              faunaDB.query.Get(
                faunaDB.query.Ref(
                  faunaDB.query.Collection(title),
                  commentData.data.parent
                )
              )
            )
            .then((parentData) => {
              // Save the name
              const receiverName = parentData.data.name;
              console.log("receiverName:", receiverName); <---- this is not

You might try putting that initial query within a try/catch statement to see if you can surface the error.

It’s done! Finally fixed! :netliconfetti:

TL;DR: I gave up on nodemailer and used emailjs instead.


@jen, when you pointed out the part in your reply, I kind of felt stupid. I had just recently fixed the same error in another part of my code, basically I had to add return before the code for it to be executed correctly. For example: commentsDB.query(faunaDB.query...) would become return commentsDB.query(faunaDB.query...). So, I was kinda optimistic and hurriedly published this commit only to find out that it still didn’t work only on production. I was getting the statement in the functions log that you mentioned you were not getting. I added another debug statement inside the condition which checks for the presence of @ and that worked too. However, the next log statement that was supposed to be logged after the mail was sent, never logged in production. No matter what I did. Everything was smooth as butter in local even now.

I was going to give up on that, but then I searched more articles and stuff, none really helped. I’m still not sure if it was me or nodemailer (or Netlify :stuck_out_tongue_winking_eye:), but the emails were not being sent. So, I finally decided to try using another tool to send the mails. After searching and reading about a few, I landed on emailjs. On the surface it seemed the same, and after some trial and error I could finally get it to work.

Here’s my updated code:

// ... previous code with nodemailer swapped for emailjs

// Store the sender as a constant
const sender = process.env.SENDINBLUE_SENDER

// Send the email
return new SMTPClient({
  port: 587,
  tls: true,
  host: 'smtp-relay.sendinblue.com',
  user: process.env.SENDINBLUE_USER,
  password: process.env.SENDINBLUE_PASS
}).sendAsync({
  from: sender,
  subject: 'New reply to your comment',
  to: receiverName + ' <' + receiverEmail + '>',
  text: 'email body',
}).then(() => {

  // Return the data of the comment for client-side processing
  return {
    statusCode: 200,
    body: JSON.stringify(commentData),
    headers: {
      'cache-control': 'public, max-age=0, must-revalidate'
    }
  }
  
})

Check in the repo here: https://github.com/Hrishikesh-K/OpinionJS/blob/main/src/functions/addComment.js. This is a different repo than the one I used in my opening post because I made many changes in these 20 days and merging them back to the old repo would have been a problem.

I really appreciate for taking the time @jen. Because, I had almost given up on this 20 days ago itself when I had posted the question here and more when I didn’t receive any response. After your reply, I got some optimism to give this another try and I’m happy it’s finally working. Hope this helps someone else too.

3 Likes

So glad you kept working on this fantastic project and that it’s working now :partying_face::partying_face::partying_face:Was a huge help that you created the test keys.

To others reading: please note that we generally do not and cannot provide this level of support in the forum- adding logs to custom code is well beyond the scope of our support. Hrishikesh is an exception to this rule because he’s a tremendous contributor who has helped tons of people here, so we are happy to fork, clone, log, whatever!

2 Likes

Also! If you decide to open source the repo and/or write a blog post about this, let us know and I’ll see if we can promote it somehow.

Yes, it’s indeed an open source project. I am publishing it as a Node Module too and also creating a Netlify Build Plugin to easily copy and deploy the functions. I’ve got promising results in most of the tests and was just able to overcome another limitation (discussed here).

I’m currently writing the documentation. The repo is here: https://www.github.com/Hrishikesh-K/OpinionJS/ and the documentation is here: https://opinionjs.netlify.app/. If someone does come across this as of now, don’t use it just yet. I’m constantly changing something, and more importantly, even the name is not finalized. So, a lot of breaking changes are expected. But if someone is willing to test and suggest changes, go ahead. I’d love to improve this thing as I think it might help many people solve a major problem on their static websites.

3 Likes

@hrishikesh how did you load emailjs? I’m giving up on nodemailer because I can’t make it work in prod either, but I’m having trouble using emailjs in testing (locally with netlify-cli).

  • I can’t const emailjs = require("emailjs");, I get an error message saying to use import() instead
  • using const { SMTPClient } = import("emailjs"); removes the previous error, but now I see TypeError: SMTPClient is not a constructor when doing new SMTPClient :person_shrugging:

any tips?
`

Hi @jcpsantiago

If you are interested in a similar thread I posted a response with a link to source code for a working function using nodemailer.

@coelmay your solution didn’t work for me, but in the end I figured it out. The point is one must handle the return value (a Promise) from nodemailer at all levels. In my case, I am using array.map() to send several emails so I had to use const results = await Promise.all(new_shipments.map(email_tracking_numbers)); at the top-level (the handler definition) and email_tracking_numbers is an async function which returns the result of calling sendMail e.g. const mail_res = await transporter.sendMail({.....}); return mail_res;

I’m not a JS person, so I’m not exactly clear why the dev environment doesn’t care about the Promises, but production does. It would be great if this quirk was explained in the documentation because there are so many questions related to this in the forums and on SO. I myself asked about axios some time ago ( Function works locally but wont POST with axios, doesn't log any errors or exceptions - #3 by jcpsantiago ) and I was still clueless when faced with essentially the same issue with nodemailer.

So for anyone finding this (even you future @jcpsantiago :wave:): handle your Promises all the way to the top, always return everything and use await Promise.all if you’re handling an array of promises.

Cheers

2 Likes

Hey there, @jcpsantiago :wave:

Thanks so much for coming back and chiming in. Your information will definitely benefit future forums members who encounter something similar (or future you!!)

Additionally, I have shared your documentation feedback with the team!

1 Like

@jcpsantiago,

Chances are emailjs might have updated or there are some other changes, but you should have been able to use it like:

import {SMTPClient} from 'emailjs'

Both care equally about promises, but the way both behave might be different. On production, the Function container is terminated almost immediately after it hits the final return statement - the one that sends the body and statusCode. So, the execution of your function is terminated and any ongoing processes are shut. Consider the following example:

async sendEmail() {
  // assume that this function needs 3 seconds to finish the task
}
sendEmail()
return {
  body: JSON.stringify({
    message: 'It worked'
  }),
  statusCode: 200
}

In this case, you call your sendEmail() function, but it takes 3 seconds to complete its task. However, your function’s thread continues to the next line which sends the body to client and terminates the container.

When you do: async sendEmail(), or return sendEmail().then(), you explicitly ask your code to “wait” till that task is done, before moving on to the next part.

The reason it works fine locally is most probably because, your local device doesn’t directly “shut down” the processing. The termination of your function might take time locally as it’s not an independent container, and thus, even though the client has received the body, the sendEmail() function is still doing its work in the background.