8
\$\begingroup\$

I have built a tiny script to send out push notifications. I would be glad to receive general suggestions / feedback.

My main problem is that the only security offered is coming from the OS filesystem settings (making the config file not readable to other users) and I would be very happy to improve my code and make it more secure.

In case anybody is much more comfortable with git than with copy/paste:

git clone https://github.com/ltpitt/python-simple-notifications.git

For the others here's the main code:

#
#  ______________________
# / Simple Notifications \
# \                      /
#  ----------------------
#     \ ^__^
#      \(oo)\_______
#       (__)\       )\/\
#         ||-----w |
#         ||      ||
#
# This script sends notification using
# Email, Pushbullet or Pushover
#
# Please put your data into configure.py before using this script

import notification_config
import requests
import json
import sys
import httplib, urllib
from email.mime.text import MIMEText
import smtplib

server = smtplib.SMTP()

def send_email(email_subject, notification_msg, email_recipients):
    '''
    This functions sends a notification using Email
            Args:
            email_subject (str) : Email Subject.
            notification_msg (str) : Email Body.
            email_recipients (str) : Email recipients.
    '''
    server.connect(notification_config.EMAIL_SERVER, notification_config.EMAIL_SERVER_PORT)
    if notification_config.EMAIL_DEBUG_LEVEL == '1':
        server.set_debuglevel(1)
    recipients = [email_recipients]
    msg = MIMEText(notification_msg)
    msg['Subject'] = email_subject
    msg['From'] = notification_config.EMAIL_SENDER
    msg['To'] = ', '.join(recipients)
    server.ehlo()
    server.starttls()
    server.ehlo
    server.login(notification_config.EMAIL_SENDER, notification_config.EMAIL_PASSWORD)
    server.sendmail(notification_config.EMAIL_SENDER, recipients, msg.as_string())
    server.quit()


def send_pushover_notification(body):
    '''
    This functions sends a notification using Pushover
        Args:
            body (str) : Body of text.
    '''
    conn = httplib.HTTPSConnection("api.pushover.net")
    conn.request("POST", "/1/messages.json",
      urllib.urlencode({
        "token": notification_config.PUSHOVER_APP_TOKEN,
        "user": notification_config.USER_KEY,
        "message": body,
      }), { "Content-type": "application/x-www-form-urlencoded" })
    response = conn.getresponse()
    if response.status != 200:
        raise Exception('Something wrong')
    else:
        print 'Sending complete'


def send_pushbullet_notification(title, body):
    '''
    This function sends a notification using Pushbullet
        Args:
            title (str) : title of text.
            body (str) : Body of text.
    '''
    data_send = {"type": "note", "title": title, "body": body}
    resp = requests.post('https://api.pushbullet.com/v2/pushes', data=json.dumps(data_send),
                         headers={'Authorization': 'Bearer ' + notification_config.PUSHBULLET_APP_TOKEN, 'Content-Type': 'application/json'})
    if resp.status_code != 200:
        raise Exception('Something wrong')
    else:
        print 'Sending complete'


def display_help():
    '''
    This functions displays the command help
    '''
    print 'Email     Example: --email "Email Subject" "Email Message" "Email recipients"'
    print 'Pusbullet Example: --pushbullet "Title" "Message"'
    print 'Pushover  Example: --pushover "Message"'


def main():
    if len(sys.argv) > 1:
        if sys.argv[1] == "--email":
            print len(sys.argv)
            if len(sys.argv) == 5:
                EMAIL_SUBJECT = sys.argv[2]
                EMAIL_MESSAGE = sys.argv[3]
                EMAIL_RECIPIENTS = sys.argv[4]
                send_email(EMAIL_SUBJECT, EMAIL_MESSAGE, EMAIL_RECIPIENTS)
            else:
                display_help()
        elif sys.argv[1] == "--pushover":
            if len(sys.argv) == 3:
                send_pushover_notification(sys.argv[2])
            else:
                display_help()
        elif sys.argv[1] == "--pushbullet":
            if len(sys.argv) == 4:
                send_pushbullet_notification(sys.argv[2], sys.argv[3])
            else:
                display_help()
        else:
            display_help()
    else:
        display_help()


if __name__ == '__main__':
    main()

And here's the config file:

# Email has been tested using smtp.gmail.com and port 587
#
# Please fill in your data and make sure this configuration file is not readable by other users

# Email notification parameters
EMAIL_PASSWORD = 'YOUR_PASSWORD'
EMAIL_SERVER = 'smtp.gmail.com'
EMAIL_SERVER_PORT = '587'
EMAIL_DEBUG_LEVEL = '1'

# Push notification parameters (Pushover)
PUSHOVER_APP_TOKEN = 'YOUR_APP_TOKEN'
USER_KEY = 'YOUR_USER_KEY'

# Push notification parameters (Pushbullet)
PUSHBULLET_APP_TOKEN = 'YOUR_APP_TOKEN'

Edit: If you are curious or find this script useful the updated version, containing many of the useful suggestions received here, is available on my GitHub

\$\endgroup\$
5
\$\begingroup\$

Since you’re willing to install third-party packages (I see a pip install -r requirements.txt step in your README), there are a couple of packages you can install that will probably be quite useful for your code.

For secure config: consider keyring

keyring provides an interface to the system keychain, so configuration is stored more securely than by mere filesystem access, and can only be accessed if the system keychain is unlocked.

So, for example, rather than hard-coding an access token, you could look it up like so:

import keyring

keyring.get_password('pushover', 'user_key')

This has the downside that it’s a bit more fiddly to set up – the user either has to use the OS’s keychain manager (if one exists), or run keyring.set_password(). You might consider providing a helper script that sets up the keychain, using getpass to ask for the password without dumping it to the console.

For argument parsing: consider docopt

You’re rolling your own argument parser, which is lots of extra code you have to write, debug and test. Within the standard library, there’s argparse, which makes this a little easier, but docopt makes it easier still. You just write the help message in a standard format, and let docopt do the fiddly business of option parsing.

I often use it with the of the module, like so:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
Simple Notifications.

Usage:
  notifications.py email <subject> <message> <recipients>
  notifications.py pushbullet <title> <message>
  notifications.py pushover <message>
"""

import docopt

arguments = docopt.docopt(__doc__)
print(arguments)

And now anybody reading the code can easily see what the interface to the script is, and all that messy argument parsing code has been cleaned up. Additionally, docopt will do validation – for example, warning the user if they enter too many arguments, something your code doesn’t do.

For HTTPS: consider requests

I’m a bit confused as to why you use requests to make the POST request in send_pushbullet_notification(), but fall back to httplib and setting up an HTTPSConnection instance for send_pushover_notification().

Wouldn’t it be easier to just use requests.post() in both instances?

While I’m looking at these two functions, two additional comments:

  • Don’t print anything on success, just return True or exit quietly. If I want to use these functions as part of a longer pipeline, it’s really inconvenient to have to suppress this printing when your function completes. Better not to do anything like that within the function, and let the caller decide how they want to notify the end user.

  • Don’t throw away the HTTP status code in your exception messages; it can be very useful for debugging why something failed. There may be more useful information in the response; that’s the minimum you should be sending.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.