DEV Community

0xkoji
0xkoji

Posted on • Edited on

Send Emails from Gmail to Discord Channel

I created a Discord server for an alumni’s mailing list since many people asked “Do we have a Slack group or a Discord server?” and there wouldn’t be any clear answers to the question.

Recently, I wrote a simple script to post emails about job hunting to our Discord server because I created a Discord server to make information more accessible and traceable for people on the list.

The steps for doing that are very simple because we can use Google Apps Script to access gmail without any complicated configuration. One thing you should know is that you must have a permission to add webhooks to a channel. If you don’t have it, there will be two ways to solve the permission issue. One is to request the permission to a Discord server owner or admin and the other is to create your own Discord server which is super easy.

Step 1. Create a channel (if you don’t have a channel)

Step 2. Create a webhook and get token

Step 3. Add a webhook and token as project properties to Google Apps Script

Step 4. Write a script

Step 5. Set a trigger (time driven)

Step 1. Create a channel

Create a new channel if you need it.

Step 2. Create a webhook and get token

This step is very easy because you just need to do the followings.

  1. Click Edit channel button
  2. Select Integrations and Click webhooks
  3. Click New Webhook to create a new one

Step 3. Add webhook and token as project properties to Google Apps Script

Actually, this step should be optional because the current Google Apps Script editor doesn’t offer a way to access project properties. So we need to switch the editor from the current to the classic version temporarily. So if you don’t want to switch the editor, you can hard -code webhook and token in your script. I prefer to use environment var instead of hard-coding.

This step is totally up to you.

  1. Click Use classic editor
  2. Click File → Project properties
  3. Select Script properties tab and add rows for a webhook and token

script properties

Step 4 Write a script

The following is what I use right now (actually I changed a couple of lines for this post).

I think the code itself will be needed to improve the performance because this code will take almost 90 seconds to finish the process. Especially, calling sendDiscord from loop isn’t good lol but I leave this because currently the mailing list isn’t received many job emails lol.

There is one thing you should keep in mind that Discord has a limitation that a free user cannot post more than 2000 characters.

function postDiscord(postMsg) {
  const props = PropertiesService.getScriptProperties();
  const webhooks = props.getProperty("WEBHOOKS"); // get value from project properties
  const token = props.getProperty("TOKEN");
  const channel = 'jobs' // channel name
  const parse = 'full';
  const method = 'post';

  const payload = {
    'token': token,
    'channel': channel,
    'content': postMsg,
    'parse': parse,
  };

  const params = {
    'method': method,
    'payload': payload,
    'muteHttpExceptions': true,
  };
  response = UrlFetchApp.fetch(webhooks, params);
}

function sendMailsToDiscord() {
  const searchQuery = 'label:mailinglist@lists.example.com and subject:job';
  const dt = new Date();
  const checkSpan = 30;
  dt.setMinutes(dt.getMinutes() - checkSpan);

  const threads = GmailApp.search(searchQuery);
  const msgs = GmailApp.getMessagesForThreads(threads);
  for(let i =0; i<msgs.length; i++) {
    const lastMsgDt = threads[i].getLastMessageDate();

    if(lastMsgDt.getTime() < dt.getTime()) {
      break;
    }

    for(let j =0; j<msgs[i].length; j++) {
      const msgDate = msgs[i][j].getDate();
      // const msgBody = msgs[i][j].getBody(); // html
      const msgBody = msgs[i][j].getPlainBody();
      const subject = msgs[i][j].getSubject()
      const postMsg = "From mailing list" + "\n" +
          Utilities.formatDate(msgDate, 'America/New_York', 'MM/DD/yyyy hh:mm:ss') + "\n" +
              "Title:" + subject + "\n" +
              "[hr]" +
               msgBody;
      console.log(`chars: ${postMsg.length}`);
      // The limit is 2000 characters
      if(postMsg.length > 2000) {
        const stopPos = 1900; // 
        const msg =  "`This message is more than 2000 chars so I cannot post the entire message. Sorry.`";
        postMsg = postMsg.substring(0, stopPos) + "\n" + msg
      }
      console.log(postMsg);
      console.log('===================================');
      console.log(`chars: ${postMsg.length}`);
      console.log('===================================');
      postDiscord(postMsg);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5. Set a trigger (time driven)

Click Timer icon and click Add Trigger in the right bottom.

In this case, select sendMailsToDiscord , Head and Time-driven then choose Minutes timer and Every 30 minutes since we set 30 minutes as check span. If you want to run the script, every 24 hours, you will need to update checkspan and select Day timer then select time from the list.

trigger settings

Top comments (13)

Collapse
 
0xkoji profile image
0xkoji

The code is already in this post. (Line 4)

Collapse
 
thomasseandominickelly profile image
Thomas Sean Dominic Kelly

Thanks for this, it is really useful and lightweight.

I've got a company account which only rarely receives emails, and I am always forgetting to check it. I have rejigged this code slightly so that it only checks for unread messages and forwards those to the specified discord channel (I've made a private channel only accessible to me on the server). Finally, it marks these messages as read so that you don't get repeat messages. Moving my email notification system to discord 🙂

function postDiscord(postMsg) {
  const props = PropertiesService.getScriptProperties();
  const webhooks = props.getProperty("WEBHOOKS"); // get value from project properties
  const token = props.getProperty("TOKEN");
  const channel = 'jobs' // channel name
  const parse = 'full';
  const method = 'post';

  const payload = {
    'token': token,
    'channel': channel,
    'content': postMsg,
    'parse': parse,
  };

  const params = {
    'method': method,
    'payload': payload,
    'muteHttpExceptions': true,
  };
  response = UrlFetchApp.fetch(webhooks, params);
}

function sendMailsToDiscord() {
  const unreadCount = GmailApp.getInboxUnreadCount();

  if(unreadCount === 0) {
    return;
  }

  const threads = GmailApp.getInboxThreads(0, unreadCount);
  const msgs = GmailApp.getMessagesForThreads(threads);
  for(let i =0; i<msgs.length; i++) {

    for(let j =0; j<msgs[i].length; j++) {
      const msg = msgs[i][j];
      const msgFrom = msg.getFrom();
      const msgBody = msg.getPlainBody();
      const msgSubject = msg.getSubject();

      const postMsg = 
      `From: ${msgFrom} \nTitle: ${msgSubject} \nBody: ${msgBody}`

      // The limit is 2000 characters
      if(postMsg.length > 2000) {
        const stopPos = 1900; // 
        const errorMsg =  "`This message is more than 2000 chars so I cannot post the entire message. Sorry.`";
        postMsg = postMsg.substring(0, stopPos) + "\n" + errorMsg
      }
      console.log(postMsg);
      console.log('===================================');
      console.log(`chars: ${postMsg.length}`);
      console.log('===================================');
      postDiscord(postMsg);
      GmailApp.markMessageRead(msg);
    }

  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
southvictor336 profile image
SouthVictor336

Does this work? How will the script find the email without a search Query?

Collapse
 
thomasseandominickelly profile image
Thomas Sean Dominic Kelly

It does indeed work, it has been running since this post in fact.

The script doesn't need to search for emails, as it just grabs the all the unread emails, forwards them to discord and finally marks them as read, so you don't get repeat notifications.

It figures out what emails are unread with this line:

  const unreadCount = GmailApp.getInboxUnreadCount();
Enter fullscreen mode Exit fullscreen mode

However, there is a bug in the code... can you see it?


postMsg is defined as a const but is reassigned if the email message is longer than 2000. That is the only change I have had to make to the script

Collapse
 
frankylee profile image
Frank Lee

Does this script still work? I've been trying to get it to work but nothing is happening. I believe I've entered in the correct webhook/token as I'm not getting any error messages when running the script. I'm seeing Execution started and Execution completed.

I've only changed two things in the the script. The channel name and the searchQuery, but it yields nothing. I also double checked that the webhook has permission to post in the channel. Any ideas to what I'm doing wrong?

const searchQuery = 'subject:Your collection day is tomorrow';

Enter fullscreen mode Exit fullscreen mode
Collapse
 
frankylee profile image
Frank Lee

Nvm, I figured out what I was doing wrong. I wasn't pushing the correct command. Thanks again for the script as it works well!

Collapse
 
0xkoji profile image
0xkoji

Nice, glad to hear that!
I'm thinking of writing a new post about this since I've received some comments.

Collapse
 
evolze profile image
Evolze • Edited

There seems to be an issue or it does not work anymore 😥 I followed the instructions (great guide btw) and when I run the script, I get the following error. Any ideas on what I'm doing wrong?

*Error Exception: *
DNS error: 1079268134118248508
postDiscord @ Code.gs:21
sendMailsToDiscord @ Code.gs:60

Here's the modification I made to the script:

function postDiscord(postMsg) {
  const props = PropertiesService.getScriptProperties();
  const webhooks = props.getProperty("WEBHOOKS"); // get value from project properties
  const token = props.getProperty("TOKEN");
  const channel = 'unraid-notifications' // channel name
  const parse = 'full';
  const method = 'post';

  const payload = {
    'token': token,
    'channel': channel,
    'content': postMsg,
    'parse': parse,
  };

  const params = {
    'method': method,
    'payload': payload,
    'muteHttpExceptions': true,
  };
  response = UrlFetchApp.fetch(webhooks, params); // First Error Message Points Here
}

function sendMailsToDiscord() {
  const searchQuery = 'from:realemailhere@gmail.com';
  const dt = new Date();
  const checkSpan = 30;
  dt.setMinutes(dt.getMinutes() - checkSpan);

  const threads = GmailApp.search(searchQuery);
  const msgs = GmailApp.getMessagesForThreads(threads);
  for(let i =0; i<msgs.length; i++) {
    const lastMsgDt = threads[i].getLastMessageDate();

    if(lastMsgDt.getTime() < dt.getTime()) {
      break;
    }

    for(let j =0; j<msgs[i].length; j++) {
      const msgDate = msgs[i][j].getDate();
      // const msgBody = msgs[i][j].getBody(); // html
      const msgBody = msgs[i][j].getPlainBody();
      const subject = msgs[i][j].getSubject()
      const postMsg = "Backups successful" + "\n" +
          Utilities.formatDate(msgDate, 'America/Denver', 'MM/DD/yyyy hh:mm:ss') + "\n" +
              "Title:" + subject + "\n" +
              "[hr]" +
               msgBody;
      console.log(`chars: ${postMsg.length}`);
      // The limit is 2000 characters
      if(postMsg.length > 2000) {
        const stopPos = 1900; // 
        const msg =  "`This message is more than 2000 chars so I cannot post the entire message. Sorry.`";
        postMsg = postMsg.substring(0, stopPos) + "\n" + msg
      }
      console.log(postMsg);
      console.log('===================================');
      console.log(`chars: ${postMsg.length}`);
      console.log('===================================');
      postDiscord(postMsg); // Second Error Message Points to Here
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
0xkoji profile image
0xkoji

The script itself still works as expected on my end.
Are you sure that you set the webhook url and token properly and pass the proper params?

Collapse
 
evolze profile image
Evolze • Edited

Yeah I double checked. The 'classic' version of the properties pane does not seem to be available anymore. Here is what I have on my end when I go to the left-hand menu bar (Project Settings --> Script Properties):

Image description

For the script properties, am I supposed to provide a URL or how is it supposed to be laid out? I ask as I followed on how to dissect the Discord webhook via its ID and TOKEN.

EDIT: I was able to get it to work "somewhat" by changing the script properties to the following:

WEBHOOKS || discord.com/api/webhooks/100000009...
TOKEN || hg_SMWJNfoo

Now when I click execute, it appears to fully execute. However, I am not receiving any messages in that Discord channel itself. Any ideas? The triggers are setup correctly. Unless it does not work when you manually execute it and just have to give it some time? Is there anything else I need to do once it's all setup? Here is what I have for the search criteria:

from: emailaddress@gmail.com

Also, see below for the script execution output:

Image description

Thread Thread
 
0xkoji profile image
0xkoji

That sounds your discord access right issue.
Are you sure that you allowed the bot/account to post a message to the channel?

Collapse
 
florisjan78 profile image
florisjan78

I am getting 401 unauthorized errors. You only see them when you set

'muteHttpExceptions': false

My variables are set as folows:
const webhooks = 'discord.com/api/webhooks/00002345140112979';
const token = 'nottherealtokenki56MxTtZW2opjtoWxuSICbOnuO';

which are the left and right part of the url from copy token in discord.

Is there a way to test if the webhook actually works?

another thing: with messages longer than 2000 I got a value assigned to const error, so I removed const from postMsg

Collapse
 
far2bribge profile image
Far2Bribge • Edited

If on the off chance you still need this, webhooks needs to include the https:// and include the token at the end: discord.com/api/webhooks/yourid/yourtoken.

To test the script, you need to run SendMailsToDiscord() and if any emails were recieved within the last 30mins (or your specified interval) it should send a discord message.

I was getting 405s for hours, turns out you need the token on the end of the url as well as a variable. If you run PostDiscord(postMsg) you should get a 400 error as the content is undefined.

Hope this helps if you still need it