This Is How You Can Develop a Decentralized Email System on the Blockchain

cover
29 Jul 2024

Blockchain technology is not just backing cryptocurrency, it has a wide range of real-world use cases. Here, we are talking about a decentralized mailing software based on blockchain technology which is unique to the traditional centralized server-based email system.

Also, it offers enhanced security, privacy, and trust.

The centralized server-based email platforms are at risk of single-point failure problems. Attackers are using different techniques like Brute Force Attacks, and IMAP protocol or email servers hacks to get unauthorized access to user’s accounts. It suggests this modern generation demands an immediate solution to control their data and privacy with a secure platform to send or receive emails.

By keeping decentralization in mind and utilizing blockchain's unique capabilities, this article teaches you to develop decentralized mailing software.

Here, we are going to build a decentralized mailing software on Rootstock’s testnet which can be ultimately deployed to Rootstock’s mainnet too. Our software will be able to send or receive emails, send files as attachments, reply to emails, and delete messages. So, it is going to be interesting. Let’s get started by creating the project’s directory say “decentralized-mail”.

📥Requirements

Before getting started in the development process, you need to have ideas about the code functionalities of the mentioned programming languages and tools.

  • Ideas about the code’s functionalities of JavaScript, Solidity, and CSS.

  • Node.js: Download and install from nodejs.org.

  • Truffle: For deploying smart contracts.

    npm install -g truffle
    

  • Pinata SDK: For interacting with Pinata

    npm install @pinata/sdk
    

  • Axios: For making HTTP requests to Pinata.

    npm install axios
    

📥Project Directory Structure

Please note the project’s directory first. Create the folders and files inside folders stepwise as follows.

Figure 1:Project's directory

📥Develop Smart Contracts

We need to create a Solidity contract file. Create DecentralizedMail.sol in the directory contracts/DecentralizedMail.sol as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DecentralizedMail {
    struct Message {
        address sender;
        address receiver;
        string content; // IPFS hash or plain text
        string imageHash; // IPFS hash of the image
        uint256 timestamp;
        bool isDeleted; // Flag to mark message as deleted
    }

    Message[] public messages;
    mapping(address => uint256[]) public userMessages;

    event MessageSent(address indexed sender, address indexed receiver, uint256 messageId);
    event MessageDeleted(address indexed user, uint256 messageId);

    function sendMessage(address _receiver, string memory _content, string memory _imageHash) public {
        uint256 messageId = messages.length;
        messages.push(Message({
            sender: msg.sender,
            receiver: _receiver,
            content: _content,
            imageHash: _imageHash,
            timestamp: block.timestamp,
            isDeleted: false
        }));
        userMessages[_receiver].push(messageId);
        userMessages[msg.sender].push(messageId); // Add message to sender's message list
        emit MessageSent(msg.sender, _receiver, messageId);
    }

    function getMessages() public view returns (Message[] memory) {
        return messages;
    }

    function getUserMessages(address _user) public view returns (uint256[] memory) {
        return userMessages[_user];
    }

    function getMessageById(uint256 messageId) public view returns (Message memory) {
        require(messageId < messages.length, "Message does not exist");
        return messages[messageId];
    }

    function deleteMessage(uint256 messageId) public {
        require(messageId < messages.length, "Message does not exist");
        Message storage message = messages[messageId];
        require(message.receiver == msg.sender || message.sender == msg.sender, "Not authorized to delete this message");
        message.isDeleted = true;
        emit MessageDeleted(msg.sender, messageId);
    }

    function getUserInbox(address _user) public view returns (Message[] memory) {
        uint256[] memory messageIds = userMessages[_user];
        Message[] memory inbox = new Message[](messageIds.length);
        uint256 counter = 0;

        for (uint256 i = 0; i < messageIds.length; i++) {
            Message storage message = messages[messageIds[i]];
            if (!message.isDeleted) {
                inbox[counter] = message;
                counter++;
            }
        }

        // Resize the array to fit the number of non-deleted messages
        Message[] memory resizedInbox = new Message[](counter);
        for (uint256 j = 0; j < counter; j++) {
            resizedInbox[j] = inbox[j];
        }

        return resizedInbox;
    }
}

Our contract provides a decentralized platform for users to send ( sendMessage function is used), retrieve (getMessages function is used), and delete messages (deleteMessage function is used) securely using Rootstock blockchain technology. Messages are stored on-chain with details such as sender, receiver, content, image hash, timestamp, and deletion status.

📥Create Migration Script

We need a migration script to simplify the smart contract development lifecycle. Its main role in our project is to automate and manage the deployment process. Create a migration script named 1_deploy_contracts.js in the root directory as migrations/1_deploy_contracts.js as follows:

const DecentralizedMail = artifacts.require("DecentralizedMail");

module.exports = function(deployer) {
    deployer.deploy(DecentralizedMail);
};

📥Configure Truffle

A Truffle configuration file is essential to define how and where our smart contract is deployed.

First, install the dependency using npm:

npm install @truffle/hdwallet-provider

It is designed to specify network settings, compiler versions, and other deployment parameters as follows:

const HDWalletProvider = require('@truffle/hdwallet-provider');
require('dotenv').config();

module.exports = {
    networks: {
        development: {
            host: "localhost",
            port: 8545,
            network_id: "*" // Match any network id
        },
        testnet: { // Renamed from rskTestnet to testnet
            provider: () => new HDWalletProvider(process.env.MNEMONIC, 'https://public-node.testnet.rsk.co'),
            network_id: 31, // RSK Testnet network id
            gas: 5500000, // Gas limit (adjust if necessary)
            gasPrice: 1000000000, // Gas price in wei (1 gwei)
            networkCheckTimeout: 10000 // Timeout for network checks
        }
    },
    compilers: {
        solc: {
            version: "0.8.0"
        }
    }
};

This configuration sets up deployment environments and compiler options. It ensures contracts are deployed and compiled correctly for development and test networks.

Note: Create .env file in the root directory. Now, you create the file with the following codes and update them later.

You need to put your .env file in the project’s root directory like .env.example file as shown in Figure 1.

MNEMONIC=your_mnemonic_phrase_here //You need to add your wallet's 12 words mnemonic phrase here
REACT_APP_PINATA_API_KEY=your_pinata_api_key
REACT_APP_PINATA_SECRET_KEY=your_pinata_secret_key
REACT_APP_CONTRACT_ADDRESS=your_rsk_testnet_contract_address

Please remember this step ** because you are updating this code later.

📥Set Up IPFS with Pinata

Our mailing app is also supporting file sharing features through the attachments. So, we are using the Upload Files feature to IPFS using Pinata. First, create the free Pinata account here.

Create a JavaScript file to allow file upload in the directory scripts/pinata-upload.js as follows:

const axios = require('axios');
const FormData = require('form-data');
require('dotenv').config();

const API_KEY = process.env.PINATA_API_KEY;
const SECRET_KEY = process.env.PINATA_SECRET_KEY;

async function uploadToPinata(content) {
    const form = new FormData();
    form.append('file', content);

    try {
        const response = await axios.post('https://api.pinata.cloud/pinning/pinFileToIPFS', form, {
            headers: {
                ...form.getHeaders(),
                'pinata_api_key': API_KEY,
                'pinata_secret_api_key': SECRET_KEY,
            },
        });

        return response.data.IpfsHash;
    } catch (error) {
        console.error('Error uploading to Pinata:', error);
        throw error;
    }
}

// Example usage
const fs = require('fs');
const filePath = './example.txt'; // Replace with your file path
const fileContent = fs.createReadStream(filePath);

uploadToPinata(fileContent)
    .then(cid => console.log('File uploaded to IPFS with CID:', cid))
    .catch(err => console.error(err));

You need the Pinata API key and secret key to update the .env file at Step** as mentioned earlier. Go to your Pinata Dashboard>API Keys>New Key, and create your new Admin API key (Tick the Admin option). Save the API keys and secret keys to a safe place.

📥Create a Front-End Interface

It is a crucial step. Our front end consists of all the basic features and codes to operate our react apps on the Rootstock testnet. To simplify the Front-end development process, I have already uploaded the codes on GitHub. You can download them from here. Also, the codes are long and our tutorial is already going to be long, you will be guided to download proper files from GitHub stepwise.

Set Up React Project

Open the terminal from your project’s directory, and enter the following commands.

npx create-react-app mail-dapp
cd mail-dapp
npm install web3 @pinata/sdk

Create React Components

In your src folder, create another components folder, and add the Upload.js file by downloading from GitHub.

Main App Component

Again, upload the App.js file in src/components/App.js directory by downloading the file from GitHub.

Configure React Entry Point

In src folder, upload index.js file from GitHub.

Now, let's go through the other components of development to assist the process of sending and receiving emails through the decentralized mailing system on the Rootstock testnet.

Make sure to download the files SendMessage.js , Inbox.js, web3.js, and index.css from the GitHub repository, and place them in the respective directories as shown in the project’s directory Figure 1.

📥Testing and Deployment

We are going to test and deploy our contract on RSK Testnet by using Truffle. Please follow the instructions stepwise.

Compile The Contract

Run the following command in the terminal.

truffle compile

You can see the following messages in the terminal if everything goes correctly.

Figure 2: Contract compilation using Truffle

Migrate (Deploy) Your Contracts

Then, run the following command to deploy the contract:

truffle migrate --network testnet

You will see something like this with the contract address.

Figure 3: Terminal output upon the successful contract migration

Please copy the contract address and update .env file in Step ** by replacing your_rsk_testnet_contract_address by your actual contract address and update the file. Please also note, after a successful contract migration, you’ll find the DecentralizedMail.json file in \decentralized-mail\build directory. Copy the JSON file, and paste it into decentralized-mail/src/utils/

📥Build the React App

Please make sure you have updated .env files with all the details correctly.

Run the following command in the terminal.

npm run build

After that, install the MetaMask extension on your browser and switch the RSK testnet by adding the following details:

Go to Select Network>Add network>Custom RPC, and then enter the following information.

  • Network Name RSK Testnet
  • New RPC URL https://public-node.testnet.rsk.co
  • ChainID (optional) 31
  • Symbol (optional) tRBTC

Now, run the following command in the terminal.

npm start

You should be redirected to the browser. The react app will invoke the MetaMask. Please approve the call. The main interface should have appeared as follows. Try sending a message to an address but please note that you have some tRBTC in your wallet to cover the gas in the RSK testnet. You can get free tRBTC from the RSK faucet.

Figure 4: Decentralized mail software main interface. The front end is communicating the RSK network through MetaMask

You can write the texts and attach files using the Choose File option. I have uploaded a photo of Nikola Tesla. Make sure to confirm the MetaMask’s call after sending the email to the recipient’s address. Wait for the transaction to be confirmed from the blockchain. Once the process is completed, you will see a message sent notice.

Let’s check whether the recipient address has my sent message in the inbox or not.

Figure 5: Receiver has successfully received a message with attached files in the RSK testnet

Perfect! You can see the sent message with the attached file of Nikola Tesla. You can reply to the inbox or also delete the inbox. Once you reply to the message, it will appear in the inbox section of the original sender.

Figure 6: Testing the "Reply" feature, A box to type the text appeared

After replying to the message, it is appeared in the inbox.

Figure 7: A replied message appeared in the inbox which indicates that the application is perfectly working

Let’s see how about the message delete feature. Once you click the delete icon, the React app invokes the MetaMask to confirm the call. Once the transaction is completed, it will be flagged and disappear from the inbox.

Figure 8: The delete feature of the mail system is working as the message has vanished from the inbox upon the confirmed transaction

In this way, we have successfully developed a blockchain-based decentralized-mail software on the RSK testnet which can also be deployed on the RSK mainnet just by modifying the codes.

Please note that this is the tutorial for developing a decentralized messaging platform with the basic functionalities. Security has been the priority since the beginning but I caution you to check everything at your own risk if you’re rushing away to develop your production-ready software.

For detailed code, please kindly refer to the GitHub repository: Decentralized Mail GitHub.

📥Common Issues and Solutions

1.EIP-1559 Not Supported

Error: Invalid value given "Eip1559NotSupportedError".

Error: Network doesn't support eip-1559.

Solution: Make sure that the gasPrice is specified in the transaction options. You should also update the frontend code to handle gas prices correctly.

  1. Metamask Not Invoked

You should install the MetaMask extension in your browser and configure the RSK testnet in your Wallet’s network configuration. Carefully inspect the code that window.ethereum.enable() is correctly used to prompt Metamask for account access.

  1. Messages Not Showing

Verify that messages are correctly pushed to the recipient’s inbox. If not, check the smart contract logic for message storage and retrieval.

Conclusion

This tutorial assist you in creating a blockchain-based decentralized messaging platform with the basic features and functionalities. With this messaging software, you can send, receive, delete messages, and reply to the messages through the blockchain. It also supports the file-sharing features by using IPFS Pinata. You can modify the code to add more features with attractive UI to meet the demands of your users.

Decentralized blockchain is backing the message-sending and receiving process with enhanced encryption which means a decentralized email exchange platform can be the best choice in the future.

It assists in protecting content from notorious phishing tactics from attackers. In addition, this sort of messaging platform is secured from the notorious single point of failure problem and other attacks from cybercriminals because the address doesn’t consist of distinguishable clues to guess the user’s personal information.