Receiving Server Notifications via Slack

In SOM we use Slack for our internal communications and to receive notifications from different channels. Since I have recently done some changes in a server I maintain, I decided to forward the different server notifications to Slack.

Server notifications are usually sent via local e-mail to root, who can read them from the  /var/mail/root file. Notifications are also tipically forwarded to an external e-mail address by configuring the  /etc/aliases file. Thus, notifications can be easily forwarded to Slack by using regular e-mails.

However, although sending e-mails to Slack is available in all plans (including the free one), the Email app is only available for paid plans. That implies that users using a free plan can only receive e-mails via direct messages from slackbot, and such e-mails cannot be automatically redirected, organized or shared in topic-specific channels participated by other users.

Nevertheless, the Incoming WebHooks app will allow us to easily send automated messages to any Slack channel using a simple shell script.

To make this work, you must login in Slack and install the Incoming WebHooks app. Once you have selected the channel in which you want to receive the notifications, you just need to configure your server using the Webhook URL provided by the app as follows.

First, the local e-mail account for root needs to be configured so that the MTA runs a specific shell script every time a local e-mail for root is sent. This can be achieved by using a pipe in the root entry at the /etc/aliases file as shown below:

# Person who should get root's mail
root: root,admin@example.com,| /opt/local/server-notifications/notify-slack.sh

The entry above states that a e-mail sent to root will be first sent locally to /var/mail/root (for archiving purposes); second, will forwarded to admin@example.com; and third, the script /opt/local/server-notifications/notify-slack.sh will be executed receiving the raw e-mail via its stdin. Remember that changes in /etc/aliases must be reloaded using the newaliases command.

Next, a possible (quick & dirty) script processing the raw e-mail is shown. It makes use of a temporal file in favor of an in-memory variable to avoid problems with line endings (also a dirty solution). The script processes the e-mail and extracts the most important fields using sed. Finally, the data is sent to Slack using the curl command in a JSON payload. It is noteworthy that the Webhook URL is configured at the beginning using the webhook_url variable.

#!/bin/bash

# URL to the Slack API
# Incoming WebHooks App URL
webhook_url="https://hooks.slack.com/services/..."
site_name="My Site Name"
site_url="https://example.com"
admin_mail="admin@example.com"


# Main function
function main {
  # We will use a temp file for simplicity
  temp_file=$(mktemp)
  # Remove temp file on exit or when receiving other signals
  trap 'rm -f -- "$temp_file"' INT TERM HUP EXIT

  # Slashes need to be escaped to avoid problems in the payload
  cat | sed -e 's/\\/\\\\/g' -e 's/"/\\\"/g' > $temp_file

  # Extract important data from raw e-mail
  body=$(cat $temp_file | sed '1,/^$/d')
  subject=$(cat $temp_file |  sed -n -e 's/^Subject: //p')
  from=$(cat $temp_file |  sed -n -e 's/^From: //p')
  to=$(cat $temp_file | sed -n -e 's/^To: //p')
  date=$(cat $temp_file | sed -n -e 's/^Date: //p')

  # Try to send message via webhook
  result=$(curl -X POST --data-urlencode "$(payload)" "$webhook_url")

  # If the webhook does not return ok, send a default message
  if [ "$result" != "ok" ];
  then
    curl -X POST --data-urlencode "$(default_payload)" "$webhook_url"
  fi
}


# Create payload using the data parsed from the raw e-mail
function payload {
cat << EOF
payload={
  "attachments" : [
    {
      "title" : "$subject",
      "text" : "\`\`\`\n$body\n\`\`\`",
      "color": "#36a64f",
      "footer" : "See the raw contents in $admin_mail's email or locally at $site_name:/var/mail/root",
      "mrkdwn_in" : [
        "text" , "fields"
       ],
      "fields": [
        {
          "title": "From",
          "value": "$from",
          "short": true
        },
        {
          "title": "To",
          "value": "$to",
          "short": true
        },
        {
          "title": "Date",
          "value": "$date",
          "short": false
        },
      ],
    },
    {
      "fallback" : "footer",
      "color": "#000099",
      "footer" : "$site_name",
      "footer_link" : "$site_url",
      "footer_icon" : "https://platform.slack-edge.com/img/default_application_icon.png",
      "ts" : "$(date +%s)",
    },
  ],
}
EOF
}

# Build a default payload whis basic data
function default_payload {
cat << EOF
payload={
  "title" : "Script error",
  "text" : "Unable to post e-mail into Slack.\nPlease check $admin_mail's <https://accounts.google.com/ServiceLogin?service=mail&passive=false&Email=$admin_mail&continue=https://mail.google.com/mail/u/$admin_mail/|e-mail> or locally at $site_name:/var/mail/root to see the raw message contents.",
  "attachments" : [
    {
      "fallback" : "footer",
      "color": "#000099",
      "footer" : "$site_name",
      "footer_link" : "$site_url",
      "footer_icon" : "https://platform.slack-edge.com/img/default_application_icon.png",
      "ts" : "$(date +%s)",
    },
  ],
}
EOF
}

# Call the main function
main "$@"

Finally, the configuration above can be tested by running the following command:

shell$ echo "Test body" | mail -s "Test subject" root@localhost

If everything is properly configured, you should receive a message from incoming-webhook in your favorite channel!