Search code examples
javascriptvue.jslambdanodemailernetlify

How to connect nodemailer to a Vue.js and Netlify app using Lambda Functions


I have a static website on Netlify built with Vue.js and I have a specific need to capture x-www-form-urlencoded data being sent from a contact route.

Because it's a spa and dynamically rendered form I have added this in both the index.html (to pre-render and allow Netlify to see the form on load)

public/index.html & src/views/Contact.vue

<form 
  netlify-honeypot="bot-field"
  name="contact"
  data-netlify="true" hidden>

    <input name="bot-field" />
    <input type="text" name="name" />
    <input type="email" name="email" />
    <input type="text" name="subject" />
    <textarea rows="5" name="message"></textarea>

</form>

The Contact view:

<form
  netlify-honeypot="bot-field"
  name="contact" method="post"
  data-netlify="true"
  @submit.prevent="handleSubmit">

  <input type="hidden" name="form-name" value="contact" />
    <p style="opacity: 0;">
      <label>Don’t fill this out if you're human: <input name="bot-field" /></label>
    </p>
    <p>
      <label>Your Name: <input v-model="form.name" type="text" name="name" /></label>
    </p>
    <p>
      <label>Your Email: <input v-model="form.email" type="email" name="email" /></label>
    </p>
    <p>
      <label>Your Subject: <input v-model="form.subject" type="text" name="subject" /></label>
    </p>
    <p>
      <label>Message: <textarea v-model="form.message" rows="5" name="message"></textarea></label>
    </p>
    <p>
      <button>Send</button>
    </p>

</form>

The data properties in the component are modelled and the method to POST off the data is as follows:

export default {
  name: 'contact',
  data: () => ({
    form: {
      name: '',
      email: '',
      subject: '',
      message: '',
    },
  }),
  methods: {
    encode (data) {
      return Object.keys(data)
        .map(
          key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`,
        )
        .join('&');
    },
    handleSubmit () {
      const axiosConfig = {
        header: { 'Content-Type': 'application/x-www-form-urlencoded' },
      };
      axios.post(
        '/',
        this.encode({
          'form-name': 'contact',
          ...this.form,
        }),
        axiosConfig,
      )
        .then(() => console.log('success'))
        .catch(e => console.error(e));
    },
  },
};

The next part of the system is the lambda function run by Netlify. Netlify allows a submission-created function every time the application receives a form submission. All you have to do is create a directory in the root of your project with the submission-created.js file in it. This directory needs to be specified in netlify.toml so Netlify knows where to look for your funcitons.

Another caveat is that if your function requires dependencies, you need to zip the folder with the node_modules included. I automate this in package.json with bestzip dependency.

package.json

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "zip": "cd functions/submission-created && bestzip ../../functions-build/submission-created.zip *",
    "clean": "rm -rf functions-build && mkdir functions-build",
    "prebuild": "npm run clean && npm run zip"
}

As you can see, the way I handle this is by writing the source of the function in functions/submission-created/submission-created.js I also have a functions-build directory which is the one specified in netlify.toml, that will contain the zipped up submission-created folder.

Basically, I npm init -y the functions/submission-created directory and npm install nodemailer, when prebuid runs, it deletes the current functions-build and re-creates it followed by zipping the functions into the directory with it's dependencies.

Here is the netlify.toml

[build]
  functions = "./functions-build"

Finally, where the important code lies, is the submission-created.js file:


exports.handler = function(event, context, callback) {
  const nodemailer = require('nodemailer');
  const querystring = require('querystring');
  const payload = querystring.parse(event.body);
  const { name, email, subject, message } = payload;

  // create reusable transporter object using the default SMTP transport
  let transporter = nodemailer.createTransport({
    host: "xxx",
    port: 587,
    secure: false, // true for 465, false for other ports
    auth: {
      user: 'xxx', // generated ethereal user
      pass: 'xxx' // generated ethereal password
    },
    tls: {
      rejectUnauthorized: false,
    },
  });

  // setup email data with unicode symbols
  let mailOptions = {
    from: `"website enquiry 👻" <${email}>`, // sender address
    to: "[email protected], [email protected]", // list of receivers
    subject: `${subject} ✔`, // Subject line
    text: `name: ${name}, \n email: ${email}, \n message: ${message}\n`, // plain text body
  };

  // send mail with defined transport object
  transporter.sendMail(mailOptions)
  .then(() => {
    callback(null, { statusCode: 200, body: 'Success' });
  })
  .catch(e => callback(e, { statusCode: 500, body: 'Error sending email' }));

};

The main problem

The form is recorded in the submissions on Netlify. The problem lies in the function:

In development (Hosting lambda function locally using netlify-lambda) I receive the properties as entered on the site.

In production, the properties are undefined. And the full event object is not logged to the console in functions.


Solution

  • The answer lies in the event param in production vs in development.

    the fix:

    // How to get post parameters in production
    exports.handler = function(event, context, callback) {
      const payload = JSON.parse(event.body).payload;
      const { name, email, subject, message } = payload;
    }