DEV Community

Diego Perez
Diego Perez

Posted on • Edited on

Google Cloud Functions: Sending emails using Gmail SMTP and Nodemailer

I have a very simple website hosted on a GCP storage segment and now I need to make it slightly less simple by adding a contact form. Then I realized that the easiest was to use everything I already have more or less in place: I could write a Function using the Google Cloud Platform that would send the emails using the GSuite account I use for the same domain. Let's see how everything goes!

The set-up

We need to use the OAuth2 authentication method, and for that to work we first need to go to the gcp console and select (or create!) a service account. Once this is done, click on the More button (the three dots) and then click Create key, this will take you to the process of creating a service key and will download a JSON file with all the data we need.

Now, we have to go to the GSuite control panel and enable de API for the service account. Go to Security -> Advanced security -> Manage API client access and enter your client_id and https://mail.google.com/ for the API scope. You can find your client_id in the JSON file.

The Google Cloud Function

To create a Google Cloud Function, we have to go to the Functions Overview page in the gcp console, click on Create function and give it a name. Then we need to select the HTTP Trigger and the Node runtime. You would also probably want to select the lower memory allocation possible. For now, we will use the inline editor, so we will later need to paste the contents of our index.js and package.json there. We need to set the Function to execute, sendMail in my case; and then we can click on more to set some environment variables. Our code will use the following:

GMAIL_ADDRESS: This is the user that we will use for authenticating, bear in mind it has to be a real user and not an alias.
CLIENT_ID: found in the JSON file.
PRIVATE_KEY: found in the JSON file.

We will also set MAIL_FROM, MAIL_TO and MAIL_BCC; these can be sent in the request but we want to have a fallback as they won't be mandatory.

The code

The only dependency we are going to have is Nodemailer, so:
npm i --S nodemailer

Now, let's take a look at the code:

exports.sendMail = (req, res) => {}

For this function to work, our sendMail method will provide two arguments: the request and the response.

  if (!req.body.subject || !req.body.text) {
    res.status(422).send({
      error: {
        code: 422,
        message: "Missing arguments"
      }
    });
    return;
  }

First thing we do is check if we have everything we need to carry on, in my case I only care if I have an actual email to send, other parameters like from or to are optional as I will store environment variables for them, but you can check for whatever you need to!

const nodeMailer = require("nodemailer");

const transporter = nodeMailer.createTransport({
  host: "smtp.gmail.com",
  port: 465,
  secure: true,
  auth: {
    type: "OAuth2",
    user: process.env.GMAIL_ADDRESS,
    serviceClient: process.env.CLIENT_ID,
    privateKey: process.env.PRIVATE_KEY.replace(/\\n/g, "\n")
  }
});

Now we create a transport with our configuration. It will take the values from the environment variables we previously defined. Note that we are doing a string replacement as the platform would have escaped the \n in our key, if we don't do this the private key won't be valid.

const mailOptions = {
  from: req.body.from || process.env.MAIL_FROM,
  to: req.body.to || process.env.MAIL_TO,
  bcc: req.body.bcc || process.env.MAIL_BCC,
  subject: req.body.subject,
  text: req.body.text
};

Now we define our mail options, again, these may be different from what you need, you can check the documentation for more detail on what's available.

transporter
    .sendMail(mailOptions)
    .then(() => {
      res.status(200).send({
        data: {
          code: 200,
          message: "Mail sent"
        }
      });
    })
    .catch(e => {
      res.status(500).send({
        error: {
          code: 500,
          message: e.toString()
        }
      });
    });

Finally, we try to send the email and return the appropriate response. You can check the full code on github

Syncing your repo

You probably noticed that there are several options to upload the code to your Function, one of them is attaching a Cloud Source repository... and that's great because then you can maintain your function just by pushing your code to the repo. My problem is I like to use GitHub, and that's not an option here... But we can mirror our repos!

We need to add a new Cloud Source repository on Google Source Repositories. Click on Add repository and select Connect external repository. Then, select the project where you have your Function and choose your connector (GitHub or Bitbucket). Next thing to do is authorize Cloud Source Repository to store your credentials, follow the steps to connect with your account and select the repo you want to associate with your Cloud Source repo. The name of the repository will have github_ or bitbucket added to the beginning of its name, and will automatically in sync with your original repository.

Once we have our Cloud Source repository ready, we can go back to our Function, click edit and select Cloud Source Repository as source: fill in the repository name and the branch or tag you want to link, and you are ready to go!

Top comments (21)

Collapse
 
heatherhosler profile image
Heather Hosler

Very helpful guide, thanks. However, I believe there is a typo in your replace function.
I think it should be replacing the single backslash with the escaped doule-backslash:
process.env.PRIVATE_KEY.replace(/\n/g, "\\n")

Collapse
 
i_maka profile image
Diego Perez

Hi Dominic,

Thank you for your suggestion. I had to double check because, I confess, it's a bit counterintuitive.

I don't remember exactly, but I think that at some point GC checks the string and, if it finds a "\n", it will add an additional backslash trying to do us a favor, hence we need to check it and remove the additional backslash to keep maintain the integrity of the private key.

Collapse
 
mahnoorgit profile image
Mahnoor-git

i am new to google function can you please explain , where and how to run "npm i --S nodemailer" command on google cloud platform.
Secondly i am getting this error "Deployment failure:
Function failed on loading user code. Error message: Node.js module defined by file index.js is expected to export function" on deploying

Collapse
 
i_maka profile image
Diego Perez

Hey there, thanks for asking!

"npm i --S nodemailer" has to be run on your local terminal, it will download the Nodemailer dependencies and update your package.json with the dependency. Your file should look like my package.json on the dependencies section.

Regarding the error, bear in mind that GCP expect you to export a method that will be run from the server, in this example I exported the method "sendMail()". Maybe is not very clear on the article, but you can check the final result of the file here. You will find the final method at the very end of the file.

Collapse
 
mahnoorgit profile image
Mahnoor-git

Thank So much for the humble reply .This issue is now resolved. Now i am facing an error {"error":{"code":422,"message":"Missing arguments"}} . G suite admin console require domain verification which i can not do , Can we use another way instead of using GSuite ?

Thread Thread
 
i_maka profile image
Diego Perez

Based on what you say, the error is possibly because you are not providing the api key... I'm afraid you will need to check if there is another way to authenticate GSuite and modify the code accordingly, but there is no option without verifying your domain, as far as I'm aware.

Thread Thread
 
mahnoorgit profile image
Mahnoor-git

Yeah seems like that.Anyways Thanks alot.

Collapse
 
skillboosttrainer profile image
SkillBoostTrainer • Edited

This is a fantastic walkthrough for setting up email functionality with Google Cloud Functions! I’ve worked on a similar use case recently where I implemented email notifications using Nodemailer and OAuth2 on GCP.

One optimization I'd suggest is leveraging Secrets Manager for securely storing sensitive values like PRIVATE_KEY and CLIENT_ID instead of environment variables, ensuring better security and easier updates.

Also, for syncing the GitHub repo, you could consider using Cloud Build triggers to automate deployments every time the repo is updated—this ensures seamless CI/CD integration.

Curious—have you explored using other serverless options like Firebase Functions for simpler hosting alongside the frontend? It might help streamline the stack for smaller projects.

Great work and thanks for sharing!

Collapse
 
vansh_shukla_60b7d232c9a5 profile image
Vansh Shukla

Great

Collapse
 
lilabennett profile image
Lila Bennett

"Great guide! Try Secrets Manager for better security!

Thread Thread
 
skillboosttrainer profile image
SkillBoostTrainer

Thank you @lilabennett I will definitely try and test it

Collapse
 
skillboosttrainer profile image
SkillBoostTrainer
Collapse
 
zakfu profile image
Zak Ainsworth

Great guide. Used this to add a contact form to my site. One step that I had to do that wasn't included here was to check the "Enable G Suite Domain-wide Delegation" checkbox when creating the service account.

Collapse
 
i_maka profile image
Diego Perez

Thank you.

That's interesting, I just checked I didn't have to enable it... Maybe your G Suite is configured in a specific way that requires it to be enabled? In my case, G Suite is configured as it comes out-of-the-box.

Collapse
 
gregfenton profile image
gregfenton

So if I have a custom domain (@domain) on Google Workspace, and I have a bunch of Firebase projects each of which I would like to be sending emails "from @domain".

In which account do I use for getting the Service Account, in which do I enable the GMail API, how do I (securely!) get the configuration into each of these different Firebase projects, etc?

Collapse
 
i_maka profile image
Diego Perez

I haven't worked with Firebase, and I'm not entirely sure what you mean by "get the configuration". If you mean the function's URL, I use environment variables in my projects.

Regarding the service account... I would use any account from @domain

Collapse
 
shahzorequreshi profile image
Shahzore Qureshi

I LOVE THIS SO MUCH. You saved me so much time. It is such a complicated procedure for something so simple. I am so happy I found this guide. You are a lifesaver!!! Thank you thank you thank you.

Collapse
 
i_maka profile image
Diego Perez

Very glad I was able to help. Thank you for sharing your feedback :)

Collapse
 
derrickrc profile image
derrickrc

Thanks for this guide! I was getting error code 500 and I realized it was because I wasn't pasting the entire private key (including the very beginning portion of '-----BEGIN PRIVATE KEY-----\n...

Collapse
 
ywroh profile image
ywroh

Thanks for the good information

Collapse
 
adhendo profile image
adhendo

Anyone having problems with their mail being sent to spam folder? Thank you for any help!
I have enabled domain wide delegation for service account...