Technology

The Right Way to Send Emails with Feathers.js

Feathers.js is a very useful mean of software development, however, not everyone knows how to make the application developed in it send emails. Have a read and find out how to do it correctly!

Paweł Pierzchlewicz
7 MIN READ
Feature image

Every modern application is in need of being able to send emails to their users. You could use a ready solution, but soon you will realise that it limits you and doesn't integrate well with your flow. At Teacode.io, we have spent years perfecting the process of sending emails to be flexible, stable and versatile. Now we are ready to share our derived flow with you.

Email Sending Process in Feathers.js

Setup

For the majority of our projects, we use Feathers.js as it allows us to deliver our clients' products faster and better. Our database of choice for this example is MongoDB with Mongoose as the interface. First, we begin with generating a new service using the following command and answering the questions.

feathers generate service

>> What type of service is it? Mongoose
>> What is the name of the service? emails
>> Which path should it be registered on? /emails
>> Does the service require authentication? Yes

This will generate the necessary file structure.

The model

Email Schema (Feathers.js)

Before we dig deeper, let's discuss the schema. First, we want to have a name for the email, which can store some unstructured information like Newsletter #142. This can be some information which you don't want the recipient to see, but it will be useful for internal purposes.

name: {
    type: String,
    required: true,
    index: true
},

Next, you need to store the addressee and recipient information. These are the from and to fields. Any SMTP provider will require this information to conduct the mail transfer.

from: {
    type: String,
    required: true,
    index: true
},
to: {
    type: String,
    required: true,
    index: true
},

Following, we have the body of the email. The subject, text and html. The text field will contain pure text data, which is displayed when the emailing service is not capable of displaying HTML data.

subject: {
    type: String,
    required: true
},
text: {
    type: String,
    required: true
},
html: {
    type: String,
    required: true
},

Then you want to manage attachments. You can have a number of them, so an array is necessary, and to properly store them, both the filename and the path is required.

attachments: [{
	filename: {
		type: String
	},
	path: {
		type: String
	}
}],

The crucial aspect comes in with the status field. We store all the emails that we want to send, regardless of their status. You will need three status types.

1) Pending - when you have prepared your email for sending, and it is waiting to be processed,
2) Complete - when you successfully sent the email; and finally -
3) Failed  - for the case when it fails to send.

This allows you to always be sure that everything goes smoothly. Just for the case when something actually goes wrong, you are maintaining a log of all the emails, so it is super simple to look up failed attempts.

status: {
    type: String,
        enum: [
            'pending',
            'complete',
            'failed'
        ],
  	default: 'pending',
	required: true,
    index: true
},

There is a tremendous amount of reasons for why your emails may fail to send. You could dump the error to your logs and scroll through all of them hoping you will find the error, or instead, you could store the error message in your email object. Joining the information about a failed email with an error message, allows you to have a quick access to the direct reason as to why an error occurred.

errorMessage: {
	type: String
}

This concludes the model for your email service. Next, we will look at adding some functionality to your emails so that you can start sending out the emails.

The Hooks

In Feathers.js hooks are the go-to place for adding logic to your services. They consist of 3 main categories: before, after and error. They are defined for all possible CRUD events: find, get, create, update, patch, remove and all.

Before Hooks

Before Hooks Graph

We will first have a look at the hooks we attach before we do something with the service. Before you create an email, you want to ensure the default values are set correctly. We handle this by using the setDefaultValues() function.

const setDefaultValues = () => {
  return async (context) => {
    if (!context.data) return context

    switch (context.method) {
      case 'create': {
        const smtpConfig = context.app.get('smtp')

        if (context.data.html && !context.data.text) {
          context.data.text = htmlToText.fromString(context.data.html)
        }

        // By default you want to send the email from the account
        // specified in the smtp config, however you allow yourself
        // to supply other data
        context.data.from = context.data.from || `${smtpConfig.from.name} <${smtpConfig.from.address}>`
        
        // The user might not define the name for the email,
        // in that case you will want to construct and informative one
        context.data.name = context.data.name || `${context.data.to} | ${context.data.subject}`
        
        // The status on creation has to be pending
        // There is no reason to create a successful or failed email
        context.data.status = 'pending'

        break
      }
      default: {
        return
      }
    }

    return context
  }
}

Let's break it down. Firstly, you want to ensure that the created request contains data; otherwise, it is not valid.

if (!context.data) return context

Then the switch double-checks whether this is used for the create method, by checking whether `context.method === 'create'`. Then you will want to extract your SMTP config object. We store it in the app's context, which we suggest you should do as well. However, let's suppose that you are using some other method of storing the information. In that case, you will need to extract the from.name and from.address.

const smtpConfig = context.app.get('smtp')

Then, unless your user provides an explicit value for the text field, you will need to parse the provided html to plain-text. For this, we use the html-to-text package.

if (context.data.html && !context.data.text) {
	context.data.text = htmlToText.fromString(context.data.html)
}

Next, you check whether the request provides you with the from data. In the case that it doesn't, you want to use the data about the account you specified in the SMTP config.

context.data.from = context.data.from || `${smtpConfig.from.name} <${smtpConfig.from.address}>`

The user might not provide you with a name for the email; in that case, you want to generate the name automatically. Then we construct it from the recipient and the subject of the email.

context.data.name = context.data.name || `${context.data.to} | ${context.data.subject}`

Finally, you need to ensure that the email is created with the pending status. Regardless of what the user sends you, always set the status to pending. There is no reason to construct emails which are by default completed or failed.

context.data.status = 'pending'

After Hooks

After Hooks Graph

Having all of the data prepared on the before hooks, we can now move on to the after hooks. Here only one hook will be in use - the create hook. The logic is that you will send the email after the email object was created in your database. The sendEmail function manages this.

import { IEmails } from '../emails.interface'
import nodemailer from 'nodemailer'
import logger from '../../../logger'

const sendEmail = () => async (context) => {
  const smtpConfig = context.app.get('smtp')
  const smtpTransport = nodemailer.createTransport(smtpConfig)

  let status = 'pending'
  let errorMessage = ''
  let error = null

  try {
    await smtpTransport.sendMail({
      from: context.result.from,
      to: context.result.to,
      subject: context.result.subject,
      text: context.result.text,
      html: context.result.html,
      attachments: context.result.attachments || undefined
    })

    status = 'complete'
  } catch (e) {
    status = 'failed'
    errorMessage = e.message
    error = e

    logger.error(e)
  }

  context.result = await context.service.patch(context.result._id, {
    status,
    errorMessage
  })

  if (error) {
    throw error
  }

  return context
}

export default sendEmail

Let's break this function down a bit. First, we define all of our variables.

// This just needs to be your smtp config, could also be a simple object
const smtpConfig = context.app.get('smtp')

// Then you define the nodemailer transport using the config
const smtpTransport = nodemailer.createTransport(smtpConfig)

// Setup the necessary variables
let status = 'pending' // This is ensured by the before hook
let errorMessage = '' // Initially you assume no error
let error = null

Having these variables, we can attempt to send the email. The attempt aspect is essential; there are multiple ways in which an email can fail to send, so you must ensure that you capture any possible errors.

try {
    // You attempt to send the email
    await smtpTransport.sendMail({
        from: context.result.from,
        to: context.result.to,
        subject: context.result.subject,
        text: context.result.text,
        html: context.result.html,
        attachments: context.result.attachments || undefined
    })
	
    // If it manages to send sendMail will not throw an error
    // So you can set the status to complete
    status = 'complete'
} catch (e) {
    // If sendMail throws an error it is captured
    
    // The status is then set to failed as well as the info on the
    // error is saved to the predifned variables.
    status = 'failed'
    errorMessage = e.message
    error = e

    logger.error(e)
}

Finally, all you are left with is to save the result to your database. You may also want to throw the error at the end if you also have some other method of managing errors in Feathers.

context.result = await context.service.patch(context.result._id, {
    status,
    errorMessage
})

if (error) {
	throw error
}

return context

This concludes the topic of sending emails in Feathers.js. You should now know how to structure your data for sending emails, preprocess all the information and send such an email. Finally, you also now know how to manage errors, which might occur on the way. Good luck :)

Subscribe to our Newsletter

Get the latest posts delivered straight to your inbox

Press to Subscribe