Search code examples
node.jsemailgoogle-oauthgmail-apiservice-accounts

how to access my own gmail api using service account in node js


I am working on a Node.js project where I need to use Gmail APIs. Can someone please explain to me how I can access my own Gmail API using a service account? I followed this Particular blog post earlier, but it seems like it's for the development mode, as the refresh_token expires every week. Whether through a service account or any other method, can someone guide me on how to access and read emails through my Gmail API? Could you kindly provide a step-by-step explanation of how this can be achieved? What setup do I need to do on the Google Cloud Console? I would be really grateful. I am working on React.js and Node.js.

the below is the code which is working perfetlly fine , but the only issue is refresh_token that is expire in every week. so that's why i think there must be a way to access the gmail api without refresh token

export class GmailService {

  async addEmail(pageToken = null) {
    try {
      const oAuth2Client = new google.auth.OAuth2(
        this.environmentService.clientId(),
        this.environmentService.clientSecret(),
        this.environmentService.googleUri(),
      );

      await oAuth2Client.setCredentials({
        refresh_token: 's1//04WEQWNyXCK6dWZEBSADWQESACgYIARAAGAQSNwF-L9IrZLU9Gxwl_8h2kMSv0drLEsLZfYGix2Yz7IuEdNCSjKST35qUt58_T_z6jK5kw1aMv7E', // I want to remove  this setup and access the gamil api using service account
      });
      const maxResults = 1;
      const url = `https://gmail.googleapis.com/gmail/v1/users/{[email protected]}/messages/?maxResults=${maxResults}&labelIds=${[
        'UNREAD',
      ]}`;

      const { token } = await oAuth2Client.getAccessToken();
      const config: any = {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      };

      const response = await axios.get(url, config);
      const labelResponse = await this.labels(config);
      const singleEmailData = response.data;
      console.log('singleEmailData', singleEmailData);
      for (const item of singleEmailData.messages) {
        const singleMessageUrl = 'https://gmail.googleapis.com/gmail/v1/users/{[email protected]}/messages/';
        const emailData = await axios.get(singleMessageUrl + `${item.id}`, config);
        const internalDate = emailData.data.internalDate;
        const emailTime = new Date(parseInt(internalDate));
        const timeString = emailTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

        const senderHeader = emailData.data.payload.headers.find((header) => header.name === 'From').value;
        const senderName = senderHeader.match(/(.+?)\s*<[^>]+>/)[1];
        const senderEmail = senderHeader.match(/[^<\s]+@[^>\s]+/)[0];
        const subject = emailData.data.payload.headers.find((header) => header.name === 'Subject').value;
        const categories = await this.prismaService.categories.findFirst({
          where: {
            labelId: {
              in: emailData.data.labelIds,
            },
          },
        });
        const markRead = await this.markEmailAsRead(item.id, config);

        if (categories) {
          emailData.data.payload.parts?.forEach(async (part, index) => {
            if (part.body.size > 0 && part.mimeType === 'text/html') {
              const bodyContent = Buffer.from(part.body.data, 'base64').toString();
              const $ = cheerio.load('<!DOCTYPE html><html><head></head><body></body></html>');
              $('body').append(bodyContent);
              const encodedImage = await this.nodeToHtml(bodyContent);
              if (!encodedImage) {
                const encodedImage = await this.nodeToHtml($.html());
                await this.prismaService.email.create({
                  data: {
                    fromEmail: senderEmail,
                    content: $.html(),
                    title: subject,
                    image: encodedImage,
                    senderName: senderName,
                    categoryId: categories.id,
                    time: timeString,
                  },
                });
              } else if (encodedImage) {
                await this.prismaService.email.create({
                  data: {
                    fromEmail: senderEmail,
                    content: $.html(),
                    title: subject,
                    image: encodedImage,
                    senderName: senderName,
                    categoryId: categories.id,
                    time: timeString,
                  },
                });
              }
              console.log('Record Added');
            }
          });
        } else {
          this.addEmail();
        }
        
      }

   
    } catch (err) {
      console.error('Error fetching emails:', err.message);
    } finally {
      setTimeout(() => {
        console.log('hello from final');
        this.addEmail();
      }, 10 * 60 * 1000);
    }
  }

  async markEmailAsRead(emailId: string, config: any) {
    const markAsReadUrl = `https://gmail.googleapis.com/gmail/v1/users/{[email protected]}/messages/${emailId}/modify`;
    const requestBody = {
      removeLabelIds: ['UNREAD'],
    };
    await axios.post(markAsReadUrl, requestBody, config);
    return true;
  }

  async getEMails() {
    try {
      const emails = await this.prismaService.email.findMany();
      return emails;
    } catch (error) {
      console.log('error', error);
    }
  }

  async nodeToHtml(content: string) {
    try {
      await fs.writeFile('index.html', content, (err: any) => {
        if (err) {
          throw err;
        }
        console.log('File is updated.');
      });
      const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
      const page = await browser.newPage();
      await page.goto(' file://' + process.cwd() + '/index.html');
      page.setViewport({ width: 300, height: 500 });
      let screenshot = await page.screenshot({ encoding: 'base64' }).then(function (data: any) {
        let base64Encode = `data:image/png;base64,${data}`;
        return base64Encode;
      });

      await browser.close();

      return screenshot;
    } catch (error) {
      console.log(error);
    }
  }
  async labels(config: any) {
    try {
      const url = 'https://gmail.googleapis.com/gmail/v1/users/{[email protected]}/labels';
      const res = await axios.get(url, config);

      for (const part of res.data.labels) {
        if (part.type === 'user') {
          const existingCategory = await this.prismaService.categories.findFirst({
            where: {
              labelId: part.id,
            },
          });

          if (existingCategory) {
            // If the category with the label ID exists, update its categoryName
            await this.prismaService.categories.update({
              where: {
                id: existingCategory.id,
              },
              data: {
                categoryName: part.name,
              },
            });
          } else {
            // If the category with the label ID does not exist, create a new category
            await this.prismaService.categories.create({
              data: {
                categoryName: part.name,
                labelId: part.id,
              },
            });
          }
        }
      }
    } catch (error) {
      console.log('labels error:', error.message);
    }
  }
}

Solution

  • I think what I would to is create a function that will get you a new refresh token. I would also have the current/last known refresh token saved as a class instance variable instead of hard coded like you do here and have a setter method for the variable. Instead do this.

    await oAuth2Client.setCredentials({
            refresh_token: this.refresh_token, // I want to remove  this setup and access the gamil api using service account
          });
          const maxResults = 1;
          const url = `https://gmail.googleapis.com/gmail/v1/users/{[email protected]}/messages/?maxResults=${maxResults}&labelIds=${[
            'UNREAD',
          ]}`;
    

    Now when your login fails because of the refresh token and you get the expected response for that failure let's assume it's an error 400 you can use an if statement to call the setter for this.refresh_token and set it to the value called by the function you created to get a new refresh_token and then reciprocally call the same function (the one that failed because of the invalid refresh token) again which should try to do the same thing again but with the new refresh_token value.