hero banner for blog post

How to build an email client

16 min read

The code in the blog post has been updated to work with Nylas API V3.

Introduction

This blog post is a step-by-step guide on how to build an email client. We will review the code for building functionality to connect an email account and read and send emails. 

We will be using the Nylas Email API to connect to the email provider (i.e. Google), which will be a thin layer added to our backend application. 

We will build our application in JavaScript, where the backend will be in nodeJS, the frontend will be in React, and we will also take advantage of the Nylas Node SDK.

Let’s get started!

Frontend development for email client

We can build an email client with Nylas using any frontend framework, especially JavaScript-based frontend frameworks. For this tutorial, we are going to use React. Here are the steps to create an initial React application:

mkdir email-client
cd email-client
mkdir frontend
cd frontend
npx create-react-app my-app
cd frontend
npm i @nylas/nylas-react dompurify sass
npm start

For the frontend, let’s break down the React code to build the following UI:

We will build the following components:

  • frontend/components/Layout.jsx component to handle loading states, screen sizes, and toast notifications and can be found on github
  • frontend/components/Toast.jsx is the notification component and can be found on github
  • /styles files can be found on github
  • frontend/components/icons are found on github
  • frontend/App.js is the root component that will render the EmailApp component
  • frontend/EmailApp.jsx consists of UI for listing all emails, showing email details, and allowing the user to compose or respond to emails
  • frontend/EmailDetail.jsx contains the contents of the email message or thread.
  • frontend/EmailList.jsx contains all the emails retrieved from the backend in a list view
  • frontend/EmailPreview.jsx preview of email shown in the list view
  • frontend/SendEmails.jsx component consisting of input fields for composing emails to send

Next we will write the code for the components listed.

App

import React, { useState, useEffect } from 'react';
import Layout from './components/Layout';
import EmailApp from './EmailApp';

function App() {
  const [isLoading, setIsLoading] = useState(false);
  const [emails, setEmails] = useState([]);
  const [toastNotification, setToastNotification] = useState('');
  const SERVER_URI = import.meta.env.VITE_SERVER_URI || 'http://localhost:9000';

  useEffect(() => {
    getEmails();
  },[])

  const getEmails = async () => {
    setIsLoading(true);
    try {
      const url = SERVER_URI + '/nylas/read-emails';
      const res = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const data = await res.json();
      if (Array.isArray(data)) {
        setEmails(data);
      } else {
        setEmails([]);
      }
    } catch (e) {
      console.warn(`Error retrieving emails:`, e);
      return false;
    }
    setIsLoading(false);
  };

  const refresh = () => {
    getEmails();
  };

  return (
    <Layout
      refresh={refresh}
      isLoading={isLoading}
      title="Email sample app"
      toastNotification={toastNotification}
      setToastNotification={setToastNotification}
    >
      <div className="app-card">
        <EmailApp
          emails={emails}
          isLoading={isLoading}
          serverBaseUrl={SERVER_URI}
          reloadEmail={refresh}
          setToastNotification={setToastNotification}
        />
      </div>
    </Layout>
  );
}

export default App;

EmailApp

import React, { useState, useEffect } from 'react';
import EmailList from './EmailList';
import EmailDetail from './EmailDetail';
import SendEmails from './SendEmails';
import './styles/email.scss';

function EmailApp({
  emails,
  isLoading,
  serverBaseUrl,
  reloadEmail,
  setToastNotification,
}) {
  const [selectedEmail, setSelectedEmail] = useState(null);
  const [draftEmail, setDraftEmail] = useState(null);

  useEffect(() => {
    setSelectedEmail(null);
  }, [emails]);

  const composeEmail = () => {
    if (draftEmail) {
      // Open the existing draft email
      setDraftEmail((prev) => ({ ...prev, isOpen: true }));
    } else {
      // Create new draft email
      const currentDate = new Date();
      const newDraft = {
        object: 'draft',
        to: '',
        subject: '',
        body: '',
        last_message_timestamp: Math.floor(currentDate.getTime() / 1000),
        isOpen: true,
      };
      setDraftEmail(newDraft);
    }
    setSelectedEmail(null);
  };

  const onEmailSent = () => {
    setDraftEmail(null);
    reloadEmail();
    setToastNotification('success');
  };

  return (
    <>
      <div className="email-app">
        {isLoading ? (
          <p className="loading-text">Loading emails...</p>
        ) : emails.length ? (
          <>
            <EmailList
              emails={emails}
              selectedEmail={selectedEmail}
              setSelectedEmail={setSelectedEmail}
              composeEmail={composeEmail}
              draftEmail={draftEmail}
              setDraftEmail={setDraftEmail}
            />
            {draftEmail?.isOpen ? (
              <SendEmails
                draftEmail={draftEmail}
                setDraftEmail={(draftUpdates) =>
                  setDraftEmail((prev) => {
                    return {
                      ...prev,
                      ...draftUpdates,
                    };
                  })
                }
                onEmailSent={onEmailSent}
                setToastNotification={setToastNotification}
                discardComposer={() => setDraftEmail(null)}
              />
            ) : (
              <EmailDetail
                selectedEmail={selectedEmail}
                serverBaseUrl={serverBaseUrl}
                onEmailSent={onEmailSent}
                setToastNotification={setToastNotification}
              />
            )}
          </>
        ) : (
          <p className="loading-text">No available email</p>
        )}
      </div>
    </>
  );
}

EmailDetail: messageReducer

Insert the below code in the <EmailDetail/> component defined next, before the component definition (i.e. function EmailDetail):

const ACTIONS = {
  SET: 'set',
  TOGGLE: 'toggle',
  SHOW_PARTICIPANTS: 'show_participants',
  REPLY: 'reply',
  FORWARD: 'forward',
  UPDATE_DRAFT: 'update_draft',
  DISCARD_DRAFT: 'discard_draft',
};

const messageReducer = (messages, { type, payload }) => {
  switch (type) {
    case ACTIONS.SET:
      return [...payload.messages];
    case ACTIONS.TOGGLE:
      return messages.map((msg) => {
        if (msg.id === payload.message.id) {
          msg = {
            ...payload.message,
            showParticipants: false,
            expanded: !msg.expanded,
          };
        }
        return msg;
      });
    case ACTIONS.SHOW_PARTICIPANTS:
      return messages.map((msg) => {
        if (msg.id === payload.message.id) {
          msg = {
            ...msg,
            showParticipants: !msg.showParticipants,
          };
        }
        return msg;
      });
    case ACTIONS.REPLY:
    case ACTIONS.FORWARD:
      return messages.map((msg) => {
        if (msg.id === payload.message.id) {
          msg = {
            ...payload.message,
            draft: {
              replyToMessageId: payload.message.id,
              subject: payload.message.subject,
              to:
                type === ACTIONS.REPLY && payload.message?.from?.length
                  ? payload.message.from[0].email || ''
                  : '',
              body: null,
            },
          };
        }
        return msg;
      });
    case ACTIONS.UPDATE_DRAFT:
      return messages.map((msg) => {
        if (msg.id === payload.message.id) {
          msg = {
            ...payload.message,
            draft: { ...payload.message.draft, ...payload.draft },
          };
        }
        return msg;
      });
    case ACTIONS.DISCARD_DRAFT:
      return messages.map((msg) => {
        if (msg.id === payload.message.id) {
          msg = {
            ...payload.message,
            draft: null,
          };
        }
        return msg;
      });
  }
};

EmailDetail

import React, { useState, useEffect, useReducer } from 'react';
import EmailIllustration from './components/icons/illustration-email.svg';
import ChevronDown from './components/icons/icon-chevron-down.svg';
import IconSync from './components/icons/IconSync.jsx';
import IconReply from './components/icons/IconReply.jsx';
import IconForward from './components/icons/IconForward.jsx';
import { formatPreviewDate } from './utils/date.js';
import { cleanEmailBody } from './utils/email.js';
import SendEmails from './SendEmails';

function EmailDetail({
  selectedEmail,
  serverBaseUrl,
  onEmailSent,
  setToastNotification,
}) {
  const [messages, dispatch] = useReducer(messageReducer, []);
  const [collapsedCount, setCollapsedCount] = useState(0);
  const [loadingMessage, setLoadingMessage] = useState('');

  useEffect(() => {
    const setupMessages = async () => {
      let showingMessages = selectedEmail.messages;
      if (selectedEmail.messages.length > 4) {
        showingMessages = [showingMessages[0], ...showingMessages.slice(-2)];
        setCollapsedCount(selectedEmail.messages.length - 3);
      }

      dispatch({
        type: ACTIONS.SET,
        payload: { messages: showingMessages },
      });

      const latestMessage = await getMessage(
        showingMessages[showingMessages.length - 1]
      );
      dispatch({ type: ACTIONS.TOGGLE, payload: { message: latestMessage } });
    };

    if (selectedEmail?.messages?.length) {
      setupMessages();
    }
  }, [selectedEmail]);

  const getMessage = async (message) => {
    if (message.body) return message;

    setLoadingMessage(message.id);
    try {
      const queryParams = new URLSearchParams({ id: message.id });
      const url = `${serverBaseUrl}/nylas/message?${queryParams.toString()}`;
      const res = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const messageData = await res.json();
      setLoadingMessage('');
      return messageData;
    } catch (e) {
      console.warn(`Error retrieving message:`, e);
      setLoadingMessage('');
      return false;
    }
  };

  const handleShowCollapsedMessages = (event) => {
    event.stopPropagation();
    if (messages.length === 3) {
      dispatch({
        type: ACTIONS.SET,
        payload: {
          messages: [
            messages[0],
            ...selectedEmail.messages.slice(1, -2),
            ...messages.slice(-2),
          ],
        },
      });
      setCollapsedCount(0);
    }
  };

  const getMessageReceivers = (message) => {
    const receiverList = [];

    if (message?.to?.length) {
      for (let i = 0; i < message.to.length; i++) {
        if (i === 3) {
          receiverList.push(`+${(message.to.length - 3).toString()}`);
          break;
        }

        const to = message.to[i];
        receiverList.push(to.email === userEmail ? 'Me' : to.name || to.email);
      }
    }
    return receiverList.join(', ');
  };

  const handleToggleMessage = async (message) => {
    if (messages.length > 1) {
      let newMsg = message;
      if (!newMsg.expanded) {
        newMsg = await getMessage(message);
      }
      if (newMsg) {
        dispatch({ type: ACTIONS.TOGGLE, payload: { message: newMsg } });
      }
    }
  };

  const handleToggleParticipants = (event, messageId) => {
    event.stopPropagation();
    dispatch({
      type: ACTIONS.SHOW_PARTICIPANTS,
      payload: { message: { id: messageId } },
    });
  };

  const handleMessageCompose = (event, message, action) => {
    event.stopPropagation();
    dispatch({
      type: action,
      payload: { message },
    });
  };

  const handleFocusComposer = (event) => {
    event.stopPropagation();
  };

  return (
    <div className="email-detail-view">
      {selectedEmail ? (
        <div className="selected">
          <h3 className="title">{selectedEmail.subject || '(no subject)'}</h3>
          <div className="label-container">
            {selectedEmail.labels?.map((label) => (
              <span key={label.id} className={`label ${label.name}`}>
                {label.display_name}
              </span>
            ))}
          </div>

          {/* Each email message can be expanded and collapsed */}
          <div className="message-list">
            {messages?.map((message, index) => {
              const isLoading = loadingMessage === message.id;
              return (
                <div
                  key={message.id}
                  className={`message-container`}
                  onClick={() => handleToggleMessage(message)}
                >
                  <div className="email-info">
                    <div className="sender-container">
                      {!!message.from?.length && (
                        <div className="sender">
                          <span className="sender-name">
                            {message.from[0].name}
                          </span>
                          {message.expanded && (
                            <span className="sender-email">
                              {message.from[0].email}
                            </span>
                          )}
                        </div>
                      )}
                      <span>
                        {formatPreviewDate(
                          new Date(Math.floor(message.date * 1000)),
                          true
                        )}
                      </span>
                    </div>
                    {message.expanded && (
                      <div
                        className={`receiver-container`}
                        onClick={(event) =>
                          handleToggleParticipants(event, message.id)
                        }
                      >
                        <span>to {getMessageReceivers(message)}</span>
                        <button className="collapse-button">
                          <img
                            className={`collapse-icon ${
                              message.showParticipants ? 'open' : ''
                            }`}
                            src={ChevronDown}
                            alt="chevron down"
                            width="10"
                          />
                        </button>
                      </div>
                    )}

                    {message.showParticipants && (
                      <div className="participants-container">
                        <div className="participants-title">From</div>
                        <div className="participants-list">
                          {message.from?.map((p) => (
                            <span key={p.email}>
                              {p.name ? `${p.name} - ` : ''}
                              {p.email}
                            </span>
                          ))}
                        </div>

                        <div className="participants-title">To</div>
                        <div className="participants-list">
                          {message.to?.map((p) => (
                            <span key={p.email}>
                              {p.name ? `${p.name} - ` : ''}
                              {p.email}
                            </span>
                          ))}
                        </div>

                        {!!message.cc?.length && (
                          <>
                            <div className="participants-title">CC</div>
                            <div className="participants-list">
                              {message.cc?.map((p) => (
                                <span key={p.email}>
                                  {p.name ? `${p.name} - ` : ''}
                                  {p.email}
                                </span>
                              ))}
                            </div>
                          </>
                        )}
                      </div>
                    )}
                  </div>
                  {message.expanded ? (
                    <div className="email-body">
                      <div
                        className="email-body-html"
                        dangerouslySetInnerHTML={{
                          __html: cleanEmailBody(message.body),
                        }}
                      />

                      {/* {!!message.files?.length && (
                        <div className="attachment-container">
                          <div className="attachment-title">
                            <span>Attachments</span>
                            <div className="line" />
                          </div>

                          <div className="attachment-files">
                            {message.files
                              .filter(
                                (file) =>
                                  file.content_disposition === 'attachment' &&
                                  !file.content_type.includes('calendar')
                              )
                              .map((f) => (
                                <div
                                  className="attachment"
                                  key={f.id}
                                  onClick={(event) =>
                                    downloadAttachment(event, f)
                                  }
                                >
                                  <img
                                    src={AttachmentIcon}
                                    alt="attachment icon"
                                    width="20"
                                  />
                                  <span>{f.filename}</span>
                                </div>
                              ))}
                          </div>
                        </div>
                      )} */}

                      {message.draft ? (
                        <div onClick={handleFocusComposer}>
                          <SendEmails
                            draftEmail={message.draft}
                            setDraftEmail={(draft) =>
                              dispatch({
                                type: ACTIONS.UPDATE_DRAFT,
                                payload: { message, draft },
                              })
                            }
                            onEmailSent={onEmailSent}
                            setToastNotification={setToastNotification}
                            discardComposer={() =>
                              dispatch({
                                type: ACTIONS.DISCARD_DRAFT,
                                payload: { message },
                              })
                            }
                            style="small"
                          />
                        </div>
                      ) : (
                        <div className="actions-container">
                          <button
                            className="outline small"
                            onClick={(event) =>
                              handleMessageCompose(
                                event,
                                message,
                                ACTIONS.REPLY
                              )
                            }
                          >
                            <IconReply />
                            Reply
                          </button>
                          <button
                            className="outline small"
                            onClick={(event) =>
                              handleMessageCompose(
                                event,
                                message,
                                ACTIONS.FORWARD
                              )
                            }
                          >
                            <IconForward />
                            Forward
                          </button>
                        </div>
                      )}
                    </div>
                  ) : (
                    <p className="snippet">{message.snippet}</p>
                  )}

                  {isLoading && (
                    <div className="loading-icon">
                      <IconSync /> Loading...
                    </div>
                  )}

                  {index !== messages.length - 1 && (
                    <div
                      className="message-border"
                      onClick={handleShowCollapsedMessages}
                    >
                      {index === 0 && collapsedCount > 0 && (
                        <span>Show {collapsedCount} messages</span>
                      )}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>
      ) : (
        <div className="empty-email">
          <img src={EmailIllustration} alt="email illustration" width="72" />
          <p>Select an email to view the message.</p>
        </div>
      )}
    </div>
  );
}

export default EmailDetail;

Email List

import React from 'react';
import EmailPreview from './EmailPreview';
import IconEdit from './components/icons/IconEdit.jsx';

function EmailList({
  emails,
  selectedEmail,
  setSelectedEmail,
  composeEmail,
  draftEmail,
  setDraftEmail,
}) {
  const handleEmailSelect = (thread) => {
    console.log('thread', thread);
    setSelectedEmail(thread);
    if (draftEmail?.isOpen) {
      setDraftEmail((prev) => {
        return { ...prev, isOpen: false };
      });
    }
  };

  const handleComposeEmail = () => {
    composeEmail();
  };

  return (
    <div className="email-list-view">
      <section className="title-container">
        <p className="title">Recent emails</p>
        <button
          className="primary small"
          onClick={handleComposeEmail}
          disabled={draftEmail?.isOpen}
        >
          <IconEdit />
          Compose
        </button>
      </section>
      <section className="email-list-container">
        {emails.length === 0 ? (
          <p>Loading emails.</p>
        ) : (
          <ul className="email-list">
            {draftEmail?.object === 'draft' && (
              <div onClick={handleComposeEmail}>
                <EmailPreview
                  thread={draftEmail}
                  selected={draftEmail?.isOpen}
                />
              </div>
            )}
            {emails.map((thread) => (
              <div key={thread.id} onClick={() => handleEmailSelect(thread)}>
                <EmailPreview
                  thread={thread}
                  selected={selectedEmail?.id === thread.id}
                />
              </div>
            ))}
          </ul>
        )}
      </section>
    </div>
  );
}

export default EmailList;

EmailPreview

import React, { useState, useEffect } from 'react';
import { formatPreviewDate } from './utils/date.js';
import AttachmentIcon from './components/icons/icon-attachment.svg';
import CalendarIcon from './components/icons/icon-calendar.svg';

function EmailPreview({ thread, selected }) {
  const [emailFrom, setEmailFrom] = useState('Unknown');
  const [hasAttachment, setHasAttachment] = useState(false);
  const [hasCalendar, setHasCalendar] = useState(false);

  useEffect(() => {
    if (thread?.messages?.length) {
      setEmailFrom(thread.messages[0].from?.[0]?.name || 'Unknown');

      checkFiles: for (const msg of thread.messages) {
        if (msg.files?.length) {
          for (const file of msg.files) {
            if (
              file.content_type.includes('calendar') ||
              file.content_type.includes('ics')
            ) {
              setHasCalendar(true);
            } else if (file.content_disposition === 'attachment') {
              setHasAttachment(true);
            }

            if (hasAttachment && hasCalendar) break checkFiles;
          }
        }
      }
    }

    if (thread?.object === 'draft') {
      setEmailFrom('(draft)');
    }
  }, [thread]);

  return (
    <li className={`email-preview-container ${selected ? 'selected' : ''}`}>
      <div className="email-content">
        <p className="sender">
          {emailFrom}
          <span className="message-count">
            {thread.messages?.length > 1 ? thread.messages?.length : ''}
          </span>
        </p>
        <div className="subject-container">
          <p className="subject">{thread.subject || '(no subject)'}</p>
        </div>
        <p className="snippet">
          {thread?.object === 'draft' ? thread.body : thread.snippet}
        </p>
      </div>
      <div className="email-info">
        {hasCalendar && (
          <img src={CalendarIcon} alt="calendar icon" width="20" />
        )}
        {hasAttachment && (
          <img src={AttachmentIcon} alt="attachment icon" width="20" />
        )}
        <div className="time">
          {formatPreviewDate(
            new Date(Math.floor(thread.last_message_timestamp * 1000))
          )}
        </div>
      </div>
    </li>
  );
}

export default EmailPreview;

SendEmails

import { useNylas } from '@nylas/nylas-react';
import React, { useState, useEffect } from 'react';
import IconDelete from './components/icons/IconDelete.jsx';

function SendEmails({
  draftEmail,
  setDraftEmail,
  onEmailSent,
  setToastNotification,
  discardComposer,
  style,
}) {
  const nylas = useNylas();

  const [to, setTo] = useState('');
  const [subject, setSubject] = useState('');
  const [body, setBody] = useState('');
  const [isSending, setIsSending] = useState(false);

  useEffect(() => {
    setTo(draftEmail.to);
    setSubject(draftEmail.subject);
    setBody(draftEmail.body);
  }, []);

  useEffect(() => {
    const updateTimer = setTimeout(function () {
      const currentDate = new Date();
      const draftUpdates = {
        to: to,
        subject,
        body,
        last_message_timestamp: Math.floor(currentDate.getTime() / 1000),
      };
      setDraftEmail(draftUpdates);
    }, 500);
    return () => clearTimeout(updateTimer);
  }, [to, subject, body]);

  const sendEmail = async ({ to, body }) => {
    try {
      const url = nylas.serverBaseUrl + '/nylas/send-email';

      const res = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ ...draftEmail, to, subject, body }),
      });

      if (!res.ok) {
        setToastNotification('error');
        throw new Error(res.statusText);
      }

      const data = await res.json();

      return data;
    } catch (error) {
      console.warn(`Error sending emails:`, error);
      setToastNotification('error');

      return false;
    }
  };

  const send = async (e) => {
    e.preventDefault();

    setIsSending(true);
    const message = await sendEmail({to, body });
    console.log('message sent', message);
    setIsSending(false);
    onEmailSent();
  };

  return (
    <form onSubmit={send} className={`email-compose-view ${style}`}>
      {!style && <h3 className="title">New message</h3>}
      <div className="input-container">
        <label className="input-label" htmlFor="To">
          To
        </label>
        <input
          aria-label="To"
          type="email"
          value={to}
          onChange={(e) => setTo(e.target.value)}
        />
        {!style && (
          <>
            <div className="line"></div>

            <label className="input-label" htmlFor="Subject">
              Subject
            </label>
            <input
              aria-label="Subject"
              value={subject}
              onChange={(e) => setSubject(e.target.value)}
            />
            <div className="line"></div>
          </>
        )}
      </div>
      <textarea
        className="message-body"
        aria-label="Message body"
        placeholder="Type your message..."
        rows={style === 'small' ? 3 : 20}
        value={body}
        onChange={(e) => setBody(e.target.value)}
      />

      <div className="composer-button-group">
        <button
          className={`primary ${style}`}
          disabled={!to || !body || isSending}
          type="submit"
        >
          {isSending ? 'Sending...' : 'Send email'}
        </button>
        <button className="icon" onClick={discardComposer}>
          <IconDelete />
        </button>
      </div>
    </form>
  );
}

export default SendEmails;

Now we’ve put together the code for the frontend application!

Backend development for email client

We can build an email client with the Nylas API using any backend language that communicates with RESTful APIs. For this tutorial, we will use the Nylas Node SDK and Express. Here are the commands to get set up before we code:

cd email-client
mkdir backend
cd backend
npm init --y
npm i body-parser cors dotenv express nylas request uuid

For the backend, let’s break down the code to communicate with the frontend and read and send emails:

  • backend/.env contains all the environment variables to integrate with the Nylas API, we will provide a template in this section, and fill in the values in the next section
  • backend/server.js is the root server file that we start using the command node backend/server.js
  • backend/route.js contains all the routes that will be integrated using the Nylas Node SDK

Next we will write the code for the files listed.

Environment

Note, we will obtain the relevant environment variables in the next section (Nylas SDK Overview).

# .env file
NYLAS_API_KEY=
# confirm your region, as we have an EU and US API URI
NYLAS_API_SERVER=https://api.us.nylas.com
NYLAS_CLIENT_ID=
USER_GRANT_ID=
USER_EMAIL=

Server.js

const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const route = require('./route');

const Nylas = require('nylas');

dotenv.config();

const app = express();

// Enable CORS
app.use(cors());

// The port the express app will run on
const port = 9000;

// Initialize the Nylas SDK using the client credentials
const NylasConfig = {
  apiKey: process.env.NYLAS_API_KEY,
  apiUri: process.env.NYLAS_API_URI,
};

const nylas = new Nylas(NylasConfig);

// Handle routes
app.post('/nylas/send-email', express.json(), (req, res) =>
  route.sendEmail(req, res)
);

app.get('/nylas/read-emails', (req, res) =>
  route.readEmails(req, res)
);

app.get('/nylas/message', async (req, res) => {
  route.getMessage(req, res);
});

app.get('/nylas/file', async (req, res) => {
  route.getFile(req, res);
});

// Start listening on port 9000
app.listen(port, () => console.log('App listening on port ' + port));

Route.js

import Nylas from 'nylas';

const NylasConfig = {
  apiKey: process.env.NYLAS_API_KEY,
  apiUri: process.env.NYLAS_API_URI,
 };
 
 const nylas = new Nylas(NylasConfig);

exports.sendEmail = async (req, res) => {
  const user = {
    grantId: process.env.USER_GRANT_ID
    emailAddress: process.env.USER_EMAIL
  };

  const sentMessage = await nylas.messages.send({
    identifier: user.grantId,
    requestBody: {
      to: [{ email: req.body.to }],
      replyTo: [{ email: user.emailAddress }],
      subject: req.body.subject,
      body: req.body.body
    },
  });

  return res.json(sentMessage);
};

exports.readEmails = async (req, res) => {
  const user = {
    grantId: process.env.USER_GRANT_ID
  }

  const user = res.locals.user;

  const threads = await nylas.threads.list({
    identifier: user.grantId,
    queryParams: {
      limit: 5,
      expanded: true,
    }
  });
  
  return res.json(threads.data);
};

exports.getMessage = async (req, res) => {
  const user = {
    grantId: process.env.USER_GRANT_ID
  }
 
  const { id: messageId } = req.query;
  
  const message = await nylas.messages.find({
    identifier: user.grantId,
    messageId,
  });

  return res.json(message.data);
};

Nylas SDK overview

We are going to use the Nylas Node SDK that allows us to connect to our users’ emails, calendars, and contacts. By doing this, we can build new user features and functionality.

To add the Nylas SDK to our backend application, we need to run the following command if we haven’t installed the Nylas SDK:

npm install nylas 

Now we have the Nylas SDK installed in our backend to start retrieving emails. Before we move on to integration, ensure you have signed up (for free!) and received your Nylas account credentials.

Integration of email client

We need first to connect a user account using the Nylas Dashboard by clicking on Add test grant:

Once you connect an account you will receive a one-time grant ID, which we will store as USER_GRANT_ID.
Take the NYLAS_CLIENT_ID that is available on the developer dashboard under the overview tab:

Next, go to the developer dashboard and generate a new API Key, NYLAS_API_KEY, from the Nylas application credentials to add to the .env file:

Now you have all the Nylas credentials to complete the .env file.

Next, all you have to do is run the backend server:

cd email-client/backend
npm start

Now you know how to build email client running using NodeJS, React, and the Nylas SDK:

Make sure you open http://localhost:3000 to access the front-end application!

Build time!

In this blog, we went over how to build an email client including both the frontend and backend environments. You can sign up for Nylas for free and start building! Continue building with Nylas by exploring quickstart guides or visiting our developer documentation.

Related resources

How to customize the Nylas Scheduler workflow with custom events

We’re excited to announce the launch of the Nylas Scheduler v3! Efficient scheduling is essential…

How to block time slots in Outlook and Google calendar with Nylas Calendar API

Key Takeaways Managing calendar availability is essential for professionals, teams, and businesses to stay organized…

How to Solve Webhook Integration Challenges with PubSub Notification Channel

Key Takeaways This article addresses the challenges of webhook integration and introduces the PubSub Notification…