Using React-email to create templates for Mailchain

Keshav SharmaKeshav Sharma
14 min read

In Web3, communication can be a complicated issue with no standard way to handle it. Many applications rely on users linking another method of contact, such as email or a phone number, which can compromise privacy and reveal identities.

That's where Mailchain comes in. It consists of an open-source, multi-chain protocol for sending encrypted messages between blockchain wallet addresses and a unified inbox application that functions just like email.

By integrating Mailchain with React-Email, we can create custom email templates that provide an even better user experience for viewing emails.

In this article, we'll walk through the process of integrating Mailchain with React-Email, create a sign-in process using OTP for added security, and share some best practices for secure and efficient email communication.

TLDR;

By integrating Mailchain with your web3 application, you can securely send emails between blockchain wallet addresses. React-email can be used to enhance the UI/UX of your emails. Access the complete source code for this article's implementation from here.

Setting up a Next.js App with react-email and Mailchain

Now that we've introduced Mailchain and React-Email, it's time to start setting up our Next.js app. Setting up a Next.js app with React-Email and Mailchain is a straightforward process that involves installing the necessary dependencies, configuring the environment variables, and setting up the necessary files and folders.

First, create a new Next.js app using:

npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app

During the initialization process, you will be prompted with a series of questions. While setting up the project, it is recommended to not choose src as we will be working with the root folder.

After making your selections, Next.js will create a new project directory called react-mailchain. You can navigate to this directory by running:

cd react-mailchain

Now we will add all the necessary dependencies related to react-email and mailchain-sdk.

Here is how to install react-email and integrate it into your project folder:

npm install react-email @react-email/button @react-email/html -E

Next, include the following script in the scripts in package.json:

scripts: {
    "email-dev": "email dev"
}

Also, create a new folder named emails in the root folder and add a file named verifyOTP.tsx:

mkdir emails
touch emails/verifyOTP.tsx

I chose to create the emails folder in the root directory because I encountered an issue running npm run email-dev when the folder was located inside the src directory. I am currently exploring potential solutions for this problem and will update the blog if I find one.

To add mailchain-sdk, it's simple:

npm install @mailchain/sdk

Setting up Redis is quick and easy with Upstash. Simply sign up and create a Redis database, which should take less than 30 seconds. For optimal security and flexibility, it's recommended to choose TLS and Eviction options during the creation process.

After setting up your database, add NEXT_PUBLIC_REDIS_URL in .env, for example:

NEXT_PUBLIC_REDIS_URL=rediss://default:********@eu2-expert-yak-30385.upstash.io:30385 # replace ***** with your DB password

Then, add ioredis to access the Redis database by:

npm i ioredis --save
npm install --save-dev @types/node

Then, add the following to your next.config.js as dns, tls, or net is available only on the server-side in Next.js:

/* next.config.js */
webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback.fs = false;
      config.resolve.fallback.dns = false;
      config.resolve.fallback.net = false;
      config.resolve.fallback.tls = false;
    }
    return config;
  },

Now that the setup is complete, it's time to start building the email template for the OTP that we'll be sending to Mailchain addresses.

Building Custom Email Templates with react-email

With react-email, creating email templates is a breeze. It's similar to writing React components but using pre-built components provided by react-email. To preview your templates and see exactly how they will look in an email client, you can use the command npm run email-dev, which we added to the scripts section in package.json during the setup process.

To explore a range of examples for templates created using react-email, you can visit the following URL: https://demo.react.email/preview/airbnb-review. This can be a helpful resource to learn and understand how react-email works. You can find a variety of examples on this page that can be used for inspiration and guidance as you build your templates.

Let's start building the template for verifying the OTP in the verifyOTP.tsx file we generated earlier.

/* emails/verifyOTP.tsx */
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Img,
  Section,
  Tailwind,
  Text,
} from "@react-email/components";

interface VerifyOTPProps {
  validationCode?: number;
}

const LOGO_URL = "https://image.lexica.art/full_jpg/1910b029-fc0c-44c2-9408-b89e77d5f345";

export const VerifyOTP = ({ validationCode }: VerifyOTPProps) => (
  <Html>
    <Tailwind>
      <Head />
      <Body className="font-sans bg-gray-50">
        <Container className="bg-white border border-gray-200 rounded-lg shadow-md mt-8 mx-auto p-8">
          <div className="flex flex-col items-center">
            <Img
              src={`${LOGO_URL}`}
              width={88}
              height={88}
              alt=""
              className="my-4"
            />
            <Text className="text-blue-500 text-xs font-bold tracking-wide uppercase mb-2">
              Verify Your Identity
            </Text>
            <Heading className="text-gray-800 text-2xl font-medium text-center mb-4">
              Enter the following code to verify your identity.
            </Heading>
          </div>
          <Section className="bg-gray-100 rounded-md px-4 py-6 flex items-center justify-center mb-6">
            <Text className="text-4xl font-bold text-gray-800 tracking-wide">
              {validationCode}
            </Text>
          </Section>
          <Text className="text-gray-600 text-base font-normal leading-6 text-center">
            If you did not request this code, please disregard this message.
          </Text>
        </Container>
      </Body>
    </Tailwind>
  </Html>
);

To build a template using react-email, you will create a <Html> component and define all the required child components inside it. In the example above, we have used the following components: <Body>, <Container>, <Img>, <Text>, <Section>, and <Heading>. We have also wrapped the entire component tree with the <Tailwind> component to enable the use of Tailwind CSS within the child components.

To make the template dynamic and include the OTP, we have added a validationCode prop to the VerifyOTP component. This prop allows us to pass the validation code as a string to the component from wherever we render it.

Once rendered, it is going to look something like this:

React-email provides a variety of components to use in building your templates. You can find more information about these components in the react-email documentation, where they have provided detailed examples of how to use each component.

Rendering React Email Templates

The render function from @react-email/render is an utility function used to render React components into HTML code that can be sent as an email. It takes a React component as input, along with any necessary props, and returns an HTML string representing the rendered email.

/* utils/renderVerifyOTP.tsx */
import { render } from "@react-email/render":

export const renderVerifyOTP = (otp: number) => {
  const html = render(<VerifyOTP validationCode={otp} />, { pretty: true });
  return html;
};

This HTML is then passed on to the mailchain utility functions discussed in the previous section. The pretty option passed to therender function is used to add whitespace and indentation to the resulting HTML for readability.

Generating and Hashing OTPs

Now, it's time to generate OTP, convert them into hashes, and then save those hashes to the Redis database.

Generating Secure OTP

The generateSecureOTP function generates a 6-digit one-time password (OTP) using randomBytes() method from the crypto package. It converts the generated random bytes into a number and pads it with leading zeros if needed to ensure it is a 6-digit number. The resulting OTP is then returned as a number.

/* utils/otpUtils.ts*/
import crypto from "crypto";

export const generateSecureOTP = (): number => {
  const buffer = crypto.randomBytes(4);
  const OTP = buffer.readUInt32BE(0) % 1000000;
  return Number(OTP.toString().padStart(6, "0"));
};

Hashing OTP

The hashOTP function takes a number (OTP) as input and returns a hashed version of it using the SHA-256 algorithm. Hashing is a one-way process that converts the input into a fixed-length string of characters, which is commonly used for storing passwords or sensitive information.

/* utils/otpUtils.ts*/
import crypto from "crypto";

export const hashOTP = (otp: number): string => {
  const hash = crypto.createHash("sha256");
  hash.update(otp.toString());
  return hash.digest("hex");
};

Storing OTP Hash

The storeOTPHash function stores the hashed OTP in Redis with an expiration time of 5 minutes. It uses the Redis client to connect to the Redis server and set the key-value pair. Finally, it disconnects from the Redis server.

/* utils/otpUtils.ts*/
import { Redis } from "ioredis";

export const storeOTPHash = async (mail: string, hash: string) => {
  const redis = new Redis(process.env.NEXT_PUBLIC_REDIS_URL!);
  await redis.set(`otpHash:${mail}`, hash, "EX", 60 * 5);
  redis.disconnect();
};

Verifying OTP Hash

The readOTPHash function retrieves the OTP hash associated with a given email address from Redis. The verifyOTP function takes an email address and an OTP as input, retrieves the OTP hash from Redis using the readOTPHash function hashes the OTP using the hashOTP function, and then compares the hashed OTP to the stored OTP hash. If they match, the function returns true.

/* utils/otpUtils.ts*/
import { Redis } from "ioredis";

export const readOTPHash = async (mail: string): Promise<string | null> => {
  const redis = new Redis(process.env.NEXT_PUBLIC_REDIS_URL!);
  const response = await redis.get(`otpHash:${mail}`);
  redis.disconnect();
  return response;
};

export const verifyOTP = async (
  mail: string,
  otp: number
): Promise<boolean> => {
  const hash = await readOTPHash(mail);
  const hashedOTP = hashOTP(otp);
  return hashedOTP === hash;
};

Getting Started with Mailchain

Let's take a moment to familiarize ourselves with the mailchain-sdk. The documentation for mailchain-sdk is user-friendly and straightforward to implement.

Mailchain requires a Secret Recovery Phrase or a Private Messaging Key to send emails to other supported Web3 identities. It is important to safeguard these sensitive pieces of information since if leaked, anyone can impersonate and send emails on your behalf.

Therefore, storing them securely is crucial to prevent unauthorized access.

To send emails via mailchain-sdk, there are two methods available for you to choose from:

  1. Using your Secret Recovery Phrase

  2. using your Private Messaging Key

Using your Secret Recovery Phrase

It will send the email through your default Mailchain (e.g. 0xkeshav@mailchain.com) that is provided to you upon creating your account. It is unique for each account.

The sendMailchainSimple() function, located in utils/mailchainUtils.tsx sends an email using Mailchain and requires a secret recovery phrase set as an environment variable. It generates a 6-digit OTP using generateSecureOTP() function from utils/otpUtils.tsx renders the OTP in a React-email template and sends the email to the recipient specified in the to parameter. The OTP is included in the subject line of the email. The function then stores the OTP hash in Redis and returns an object with the data and error properties.

/* utils/mailchainUtils.ts */
export const sendMailchainSimple = async (to: string) => {
  const secretRecoveryPhrase = process.env.NEXT_PUBLIC_SECRET_RECOVERY_PHRASE!;

  if (secretRecoveryPhrase === undefined) {
    throw new Error("You must provide a secret recovery phrase in .env");
  }

  const mailchain = Mailchain.fromSecretRecoveryPhrase(secretRecoveryPhrase);

  const fromAddress = await mailchain.user();
  const code = generateSecureOTP();  // generating 6 digits OTP using crypto.randomBytes(4)  
  const emailHtml = renderVerifyOTP(Number(code));  // rendering React-email component to HTML

  // sending mail 
  const { data, error } = await mailchain.sendMail({
    from: fromAddress.address,
    to: [to],
    subject: `Your verification code for SwiftSignup registration: ${code}`,
    content: {
      text: `Your verification code for SwiftSignup registration: ${code}`,
      html: emailHtml,
    },
  });

  await storeOTPHash(to, hashOTP(Number(code)));  // storing OTP hash to redis
  return { data, error };
};

Using your Private Messaging Key

It will send the email through the mail address associated with that private messaging key. Using this approach, you can send emails using different identities, such as an ENS domain (e.g., 0xkeshav.eth@ens.mailchain.com) or a LENS domain (e.g., 0xkeshav.lens@lens.mailchain.com), utilizing their respective private messaging key, which can be found in your Mailchain settings.

Note: In case you have doubts about the security of your private messaging key, it is advisable to register your wallet or protocol address again to create a new private messaging key.

sendMailchainSpecificAddress function is similar to sendMailchainSimple but uses a private messaging key instead of a secret recovery phrase. It generates a 6-digit OTP, renders it in a React-email template, sends it to the specified recipient address using the private messaging key, stores the OTP hash in Redis, and returns an object indicating the result of sending the email.

  •     /* utils/mailchainUtils.ts */
        export const sendMailchainSpecificAddress = async (toAddress: string) => {
          const pvtMsgKeyHex = process.env.NEXT_PUBLIC_PRIVATE_MESSAGING_KEY!;
    
          if (pvtMsgKeyHex === undefined) {
            throw new Error("You must provide a private messaging key in .env");
          }
    
          const privateMessagingKey = privateMessagingKeyFromHex(pvtMsgKeyHex);
    
          const mailSender = MailSender.fromSenderMessagingKey(privateMessagingKey);
    
          const fromAddress = process.env.NEXT_PUBLIC_SENDER_ADDRESS!;
    
          if (fromAddress === undefined) {
            throw new Error("You must provide a sender address along with its private messaging key in .env");
          }
    
          const code = generateSecureOTP();  // generating 6 digits OTP using crypto.randomBytes(4) 
          const emailHtml = renderVerifyOTP(Number(code));  // rendering React-email component  to HTML
    
          // sending mail
          const { data, error } = await mailSender.sendMail({
            from: fromAddress,
            to: [toAddress],
            subject: `Your verification code for SwiftSignup registration: ${code}`,
            content: {
              text: `Your verification code for SwiftSignup registration: ${code}`,
              html: emailHtml,
            },
          });
    
          await storeOTPHash(toAddress, hashOTP(Number(code)));  // storing OTP hash to redis   
          return { data, error };
        };
    

Now that we have a good understanding of how Mailchain works, we can move on to generating the necessary APIs and creating frontend components that allow users to send OTPs to their Mailchain address through our API.

Building Handlers for Sending Emails through Mailchain and Verifying OTPs

We will be required to generate three API Handlers:

  1. Send Mail through Secret Recovery Phrase

  2. Send Mail through Private Messaging Key

  3. Verifying OTPs

Send Mail through Secret Recovery Phrase

This route handler accepts a POST request with a to parameter representing the Mailchain address to send an email to. It then calls the sendMailchainSimple function from the mailchainUtils module to send an email through Mailchain. If the function is successful, it returns a response with the data property containing the sent email information, else it returns a response with the error property containing the error message.

/* pages/api/sendMailchainSimple.ts */
import { sendMailchainSimple } from "@/utils/mailchainUtils";
import { SentMail } from "@mailchain/sdk";
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  data?: SentMail | undefined;
  error?: string;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  try {
    const params = req.body;

    const { data, error } = await sendMailchainSimple(params.to);

    if (error) {
      throw new Error("Mailchain error: " + error.message);
    }

    res.status(200).send({ data });
  } catch (error) {
    if (error instanceof Error) {
      res.status(500).send({ error: error.message });
    }
  }
}

Send Mail through Private Messaging Key

This route handler handles a request and sends an email to a specific address using Mailchain's private messaging key.

/* pages/api/sendMailchainSimple.ts */
import { sendMailchainSpecificAddress } from "@/utils/mailchainUtils";
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  data?: any;
  error?: string;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  try {
    const params = req.body;
    const { data, error } = await sendMailchainSpecificAddress(params.to);

    if (error) {
      throw new Error("Mailchain error: " + error.message);
    }

    res.status(200).send({ data });
  } catch (error) {
    if (error instanceof Error) {
      res.status(500).send({ error: error.message });
    }
  }
}

Verifying OTPs

This route handler verifies the received OTP against the hashed OTP stored in Redis for a particular email address. It receives the OTP and email from the request body and uses the verifyOTP() function from the otpUtils module to verify it. Finally, it returns a JSON response with a result field indicating whether the OTP was valid or not.

import { verifyOTP } from "@/utils/otpUtils";
import { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { otp, mail } = req.body;

  const result = await verifyOTP(mail, Number(otp));

  res.status(200).json({ result });
}

Note: Deploying the app on Vercel may result in a gateway timeout error due to the mailchain-sdk taking more than 5 seconds to respond to requests, and Vercel having a timeout limit of 5 seconds for its free tier. This could lead to a 504 gateway timeout error.

Integrating Mailchain and OTP Handlers with Frontend

Now, it's time to put your hard work to the test by building two components: Signin.tsx and Otp.tsx, which we will use in our pages/index.tsx. You can easily copy the source code of both components and the pages/index.tsx file to speed up the process.

We will still discuss the two most important parts of these components i.e. handleSubmit() function.

Signin.tsx

It sends a POST request using Axios to the /api/sendSimpleMail endpoint to send an email to the entered email address. If the email is sent successfully, it sets the mailchainAddr state to the entered email and triggers the isSignin state to true to show the OTP input field.

/* components/Signin.tsx */
// ... (more code above)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);

    try {
    // Axios Post request to send mail when hit submit button
        const { data: sendMail } = await axios({
        method: "POST",
        url: "/api/sendSimpleMail",
        data: {
          to: email,
        },
      });

      if (!sendMail.savedMessageId) {
        console.log("Mail sent successfully!");
        setMailchainAddr(email);
        setIsSignin(true);
        }
    } catch (error) {
      console.error(error);
      }

    setIsLoading(false);
};
// ... (more code below)

Otp.tsx

It handles the OTP form submission by sending a POST request to the server to verify the OTP entered by the user. The result is logged to the console and if successful, the user is set as verified.

/* components/Otp.tsx */
// ... (more code above)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Here you can add your API call to verify the OTP and complete the signin process

    const { data } = await axios({
      method: "POST",
      url: "/api/verifyOTP",
      data: {
        mail: mail,
        otp: otp,
      },
    });

    console.log(data.result);
    if (data.result) {
      setVerified(true);
    } else {
      setOTP("");
      setVerified(false);
    }
  };
// ... (more code below)

Now that we have developed both the frontend and backend, it's time to run the command npm run dev to test our application.

The Final Look of Our App

Check out the final result of our Mailchain sign-in flow implemented with React-Email and OTP verification. Watch the GIF below to see how the app flows from start to finish:

Conclusion

In conclusion, Mailchain provides a secure way of sending emails to web3 identities. By integrating Mailchain with React-Email, we can customize the email templates and improve the user experience of viewing emails. Additionally, using OTP for authentication provides an added layer of security to the sign-in process.

However, it is important to remember that sensitive information such as the Secret Recovery Phrase or Private Messaging Key must be securely stored to prevent unauthorized access. Overall, Mailchain and React-Email can help us achieve secure and efficient email communication in the web3 world.

I hope you found this article helpful and informative. If you enjoyed it, be sure to like and follow for more articles in the future. If you have any questions, you can find me on Twitter. Thank you for reading!"

14
Subscribe to my newsletter

Read articles from Keshav Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Keshav Sharma
Keshav Sharma