How to Send an Email with Image Attachment in Node.js & React

Hayk Yaghubyan
Nov 26 · 7 min read

In the modern web, it becomes more and more popular having a contact form with an image attachment feature. You might ask why do I need it? Well, what if your customer found a bug and wants to send a screenshot to you? It makes sense right? There are many tutorials on the internet about sending emails with Node.js and Nodemailer but none of them covers the sending an attachment with it. We will go a step further and build a contact form with image attachment using Node.js Express with Nodemailer for back-end and React with Redux for Front-end. Let’s get started:

Setting up back-end

Firstly we need to set up our project:

npm init

So I assume you are already familiar with generating projects with Node.js and React, so will not go further for basic topics. We need to install the Express framework, body-parser and other dependencies.

npm install express body-parser nodemailer cors

Let’s create our main server file called index.js:

const express = require('express');const bodyParser = require('body-parser');const nodemailer = require('nodemailer');const cors = require('cors');const app = express();const port = 4444;app.use(bodyParser.json({ limit: '10mb', extended: true }))app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }))app.use(cors());app.listen(port, () => {console.log('We are live on port 4444');});app.get('/', (req, res) => {res.send('Welcome to my api');})

It’s a simple Node.js API powered by Express. After creating it we need to take the all benefit from Nodemailer and include the controller function that will send the submitted email with the image.

Creating contact controller function

We are going to use Gmail for authentication. However, you can use Sendgrid’s API which offers a better and more secure option. We are creating it in the Index.js since there is no any other functionality. We need just this simple controller and API route for sending post requests.

app.post('/api/v1/contact', (req, res) => {var data = req.body;var smtpTransport = nodemailer.createTransport({service: 'Gmail',port: 465,auth: {user: 'username',pass: 'password'}});var mailOptions = {from: data.email,replyto: data.email,to: 'goshareitio@gmail.com',subject: data.title,html: `<p>${data.email}</p><p>${data.message}</p>`,attachments: [{filename: data.title + ".jpg",contentType:  'image/jpeg',content: new Buffer.from(req.body.image.split("base64,")[1], "base64"),}]};smtpTransport.sendMail(mailOptions,(error, response) => {if (error) {res.status(400).send(error)} else {res.send('Success')}smtpTransport.close();});})

Inside the controller function, you will see the attachment field with supported file types. This is a built-in feature that comes with Nodemailer.

It’s time to move on Front-end.

Creating React.js project

We are going to use Create React App for generating our project.

npx create-react-app Contact

I guess you are familiar with react and have some knowledge so will not go further for explaining project structure. While this tutorial covers a small part for a real-world project we are going to create reusable components because it’s predictable that you are going to use these reusable components not only for contact form but also for other forms in the project like sign-in sign-up. posting, commenting and so on…

Creating reusable form components

Firstly we will start with simple input component:

import React from ‘react’;export const ProjectInput = ({
input,
label,
type,
symbol,
className,
meta: { touched, error, warning }
}) => (
<div className=’form-group’>
<label>{label}</label>
<div className=’input-group’>
{symbol &&
<div className=’input-group-prepend’>
<div className=’input-group-text’>{symbol}</div>
</div>
}
<input {…input} type={type} className={className} />
</div>
{touched &&
((error && <div className=’alert alert-danger’>{error}</div>))}
</div>
)

Then will create TextArea

import React from ‘react’;export const ProjectTextArea = ({
input,
label,
type,
rows,
className,
meta: { touched, error, warning }
}) => (
<div className=’form-group’>
<label>{label}</label>
<div className=’input-group’>
<textarea {…input} type={type} rows={rows} className={className}></textarea>
</div>
{touched &&
((error && <div className=’alert alert-danger’>{error}</div>))}
</div>
)

It’s time to create the most important reusable component of this form, an input for uploading the image.

Creating ImgFileUpload component

We are going to use FileReader. It is an object with the sole purpose of reading data from Blob (and hence File too) objects.

It delivers the data using events, as reading from disk may take time.

import React from ‘react’;export class ImgFileUpload extends React.Component {constructor() {
super();
this.setupReader()this.state = {
selectedFile: undefined,
imageBase64: ‘’,
initialImageBase64: ‘’,
pending: false,
status: ‘INIT’,
}
this.onChange = this.onChange.bind(this);
}
setupReader() {
this.reader = new FileReader();
this.reader.addEventListener(‘load’, (event) => {
const { initialImageBase64 } = this.state;
var { changedImage } = this.props;
const imageBase64 = event.target.result;
changedImage(imageBase64);
if (initialImageBase64) {
this.setState({ imageBase64 });
} else {
this.setState({ imageBase64, initialImageBase64: imageBase64 });
}
});
}
onChange(event) {
const selectedFile = event.target.files[0];
var { checkImageState } = this.props;
if (selectedFile) {
checkImageState(‘selected’);
} else {
checkImageState(‘unselected’);
}
if (selectedFile) {
this.setState({
selectedFile,
initialImageBase64: ‘’
});
this.reader.readAsDataURL(selectedFile);
}
}
render() {return (
<div className=’img-upload-container’>
<label className=’img-upload btn’>
<span className=’upload-text’> Select an image </span>
<input type=’file’
accept=’.jpg, .png, .jpeg’
onChange={this.onChange} />
</label>
</div>
)
}
}

There are many 3rd party libraries that you might find for file selection and uploading, however, it’s recommended to ignore those libraries. Some of them cause different issues from which the most popular is that you can’t create a production build because of a single 3rd party library. We got a similar issue when used 3rd party package for Angular in the past. So as fewer libraries you use as fewer issues will be in the future.

Building a complete contact form

We need to put all the above-created components in one place for getting our final form but before that, we need to install some dependencies.

npm install --save redux react-redux redux-form axios

then we need to create an index.js file that will contain all our reducers as well as our formReducer

import { createStore, compose, combineReducers } from 'redux';import { reducer as formReducer } from 'redux-form';export const init = () => {const reducer = combineReducers({form: formReducer,});const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(reducer);return store;}

Then don’t forget to implement our store inside App.js:

import React, { Component } from 'react';import { BrowserRouter, Route } from 'react-router-dom';import { Provider } from 'react-redux';import Contact from './components/contact';import './App.css';const store = require('./reducers').init();class App extends Component {render() {return (<Provider store={store}><BrowserRouter><div className='App'><div className='container'><Route exact path='/' component={Contact} /></div></div></BrowserRouter></Provider>);}}export default App;

Now, we can continue building our final contact form.

import React from 'react';import { Field, reduxForm } from 'redux-form';import { ProjectInput } from './ProjectInput';import { ProjectTextArea } from './ProjectTextArea';import { ImgFileUpload } from './ImgFileUpload';class ContactForm extends React.Component {state = {imageState: false}render() {const {handleSubmit,pristine,submitting,submitCb,valid,SetImage,loading} = this.props;return (<form onSubmit={handleSubmit(submitCb).bind(this)}onClick={this.resetValues}><Fieldname="email"type="email"label='Email'className='form-control'component={ProjectInput}/><Fieldname="title"type="text"label='Title'className='form-control'component={ProjectInput}/><Fieldname="message"type="text"label='Description'rows='6'className='form-control'component={ProjectTextArea}/><Fieldname="image"label='Image'className='form-control'component={ImgFileUpload}props={{changedImage: (e) => {SetImage(e);this.setState({imageState: true})},checkImageState: (e) => {if (e === 'selected') {this.setState({imageState: true});} else {this.setState({imageState: false});}},}}key={this.props.key}/>{loading ?<buttonclassName='btn btn-primary'type="button"disabled={true}>Sending...</button>:<buttonclassName='btn btn-primary'type="submit"disabled={!valid || pristine || submitting || !this.state.imageState}>Send</button>}</form>)}}const validate = values => {const errors = {};if (!values.email) {errors.email = 'Please enter email!';}if (!values.title) {errors.title = 'Please enter title!';}if (!values.message) {errors.message = 'Please enter message!';}return errors;}export default reduxForm({form: 'ContactForm',validate})(ContactForm)

We used reusable components which created earlier and used redux-form to power our form. Also, we implemented some validations for this form however, since it’s a completely different topic, I will recommend adding even more validations for each field separately. Besides that, we disabled the send button until all fields will be filled. Looks lots of work done right? As the final step for making our contact form completely functional, we are going to create a container component for it.

import React from 'react';import ContactForm from './ContactForm';import { reset } from 'redux-form';import axios from 'axios';import { connect } from "react-redux";import { ToastContainer, toast } from 'react-toastify';class Contact extends React.Component {constructor(props) {super(props);this.state = {errors: [],note: '',loading: false}this.pristine = false;this.Send = this.Send.bind(this);}SetImage = async (image) => {await this.setState({ image });};Send(userData) {let { image } = this.state;userData = { ...userData, image };this.setState({ loading: true });this.sendEmail(userData).then(submited => {toast.success('Email sent successfully');this.props.dispatch(reset('ContactForm'));this.setState({ key: 'cleared' })this.setState({ note: 'Email sent successfully', loading: false });},).catch(errors => {toast.error('Error occured')this.setState({ errors, loading: false })});};sendEmail = async emailData => {console.log(emailData);return axios.post('/api/v1/contact', emailData).then(res => res.data,err => Promise.reject(err.response.data.errors))};render() {const { errors } = this.state;return (<section id='contact'><ToastContainer /><div className='bwm-form'><div className='row'><div className='col-md-5'><h1>Contact Us</h1><ContactFormloading={this.state.loading}submitCb={this.Send}errors={errors}SetImage={this.SetImage}pristine={this.pristine}key={this.state.key}/></div></div></div></section>)}}export default connect(null, null)(Contact);

In the above component, we send data to the back-end. If the email was sent successfully the form is getting reset automatically. Also, we used a toaster library for showing an alert however you can just ignore that library. As you see we used Axios for doing a post request and sending data to the back-end. As far as Redux-form supports only resetting basic fields we created the functionality of resetting file input from scratch by using a special key in the contact form and contact components.

That’s it! Hope this tutorial was helpful.
You can download the full source code here:
Github

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade