6
\$\begingroup\$

This is the first program I've ever made in Python, and I never really studied the language (just looked at bits of code online) so I'm sure the performance is less than optimal.

The Objective: At my college, security alerts are occasionally sent to our phones, and sometimes posted to our subreddit for discussion. To automate this, I set up my phone to forward relevant texts to an email address, then take the message from the email address and post to Reddit. The script scans my inbox every 30 seconds for unread emails and posts the newest one if it finds anything.

Issues

  1. I wanted to use something like twilio to avoid the email address part, but I can only sign up for Alerts with one phone number, and I still want to receive alerts on my phone
  2. Too many points of failure. My phone could be off, my computer (script host) could be off/asleep, text forwarding could fail, gmail could be down, etc.
  3. Scanning the inbox every 30 seconds seems needlessly excessive and I wish there was a better way to do it.
  4. There are a lot of while loops, but I don't know any other way to catch exceptions.

I'm planning on putting the script onto a Raspberry Pi 3 later on so there's something dedicated running it, but I'd like to optimize the code as much as possible before I do that. I also had to remove the OAuth codes for security reasons.

import praw
import imaplib
import email
import time
import getpass
import RUAlerts
from datetime import datetime
app_id = 'xxxxxxxxxxxx'
app_secret = 'xxxxxxxxxxxx'
app_uri = 'xxxxxxxxxxxx'
app_scopes = 'account creddits edit flair history identity mysubreddits privatemessages read report save submit subscribe vote'
app_ua='xxxxxxxxxxxx'
app_account_code = 'xxxxxxxxxxxx'
app_refresh = 'xxxxxxxxxxxx'
def login():
    r = praw.Reddit(app_ua)
    r.set_oauth_app_info(app_id, app_secret, app_uri)
    r.refresh_access_information(app_refresh)
    return r

mail = imaplib.IMAP4_SSL('imap.gmail.com')

while True:
    try:
        emailpass = getpass.getpass('Please enter the password for xxxxxxx@xxxxx: ')
        mail.login('xxxxxxxxxxxx@gmail.com', emailpass)
        break
    except imaplib.IMAP4.error:
        print('Incorrect password')

mail.select("inbox")

while True:
    try:
        r = RUAlerts.login()

        while 1:
            result, response = mail.uid('search', None, "(UNSEEN)")
            unread_msg_nums = response[0].split()

            result, data = mail.uid('search', None, "ALL")
            latest_email_uid = data[0].split()[-1]
            result, data = mail.uid('fetch', latest_email_uid, '(RFC822)')
            raw_email = data[0][1]

            email_message = email.message_from_bytes(raw_email)

            if len(unread_msg_nums)>0:
                print('\t' + str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ' - Something\'s wrong!')
                for part in email_message.walk():
                    if part.get_content_type()=='text/plain':
                        Alert=part.get_payload()
                        while True:
                            try:
                                r.submit(subreddit='xxxxxxxxxxxx',title=Alert,text=str(Alert)+"\n \n ******** \n \n*^^I ^^am ^^a ^^bot. ^^For ^^any ^^questions, ^^comments, ^^or ^^concerns, ^^please ^^email [^^xxxxxxx@xxxxx](mailto://xxxxxxx@xxxxx)*")
                                print('\t' + str(Alert),end=' ')
                                break
                            except praw.errors.ExceptionList as e:
                                print('\tReddit error!' + str(e) + '\tRetrying in 5 minutes - ' + str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
                                ##mail.uid('STORE', latest_email_uid, '-FLAGS', '\SEEN')
                                time.sleep(300)
            else:
                print(str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ' - All clear on the RU front')
                time.sleep(30)
            break
    except:
        print('\t' + str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ' - No connection! Retrying in 5 minutes')
        time.sleep(300)
\$\endgroup\$
3
\$\begingroup\$

My main comment is that you should separate the different concerns of you code into descriptive functions. This will make it a lot more readable (and re-usable).

One comment before I get started: Your login function, which looks like it would log you in with the Reddit API seems to be unused at the moment. I guess this is a copy&paste error from censoring?


Your first concern is to log-in with Gmail to get the mail object. This can be pasted directly into a separate function:

def mail_login():
    mail = imaplib.IMAP4_SSL('imap.gmail.com')
    while True:
        try:
            emailpass = getpass.getpass(
                'Please enter the password for xxxxxxx@xxxxx: ')
            mail.login('xxxxxxxxxxxx@gmail.com', emailpass)
            break
        except imaplib.IMAP4.error:
            print('Incorrect password')
    mail.select("inbox")
    return mail

The second task, which is repeated quite often, is to print a message with the current time-stamp preceding it:

def log(text):
    print('\t{:%Y-%m-%d %H:%M:%S} - {}'.format(datetime.now(), text))

Note that I used the custom format options of str.format here.


Another task is to post an alert to Reddit, once it is found:

def post_alert(alert, r):
    alert_text = """
    {}

     ********

     *^^I ^^am ^^a ^^bot. ^^For ^^any ^^questions, ^^comments, ^^or ^^concerns, ^^please ^^email [^^xxxxxxx@xxxxx](mailto://xxxxxxx@xxxxx)*
     """
    while True:
        try:
            r.submit(subreddit='xxxxxxxxxxxx', title=alert,
                     text=alert_text.format(alert))
            print('\t{}'.format(alert), end=' ')
            break
        except praw.errors.ExceptionList as e:
            log('Reddit error! {}'.format(e))
            time.sleep(300)

I build the alert text first and filled it with str.format and used the log function.


The second to last task is to search in your emails for new messages and yield all alert texts:

class ShortTimeOut(Exception):
    pass


def search_for_alerts(mail):
    result, response = mail.uid('search', None, "(UNSEEN)")
    unread_msgs = response[0].split()

    if not unread_msgs:
        raise ShortTimeOut
    else:
        log('Something\'s wrong!')
        result, data = mail.uid('fetch', unread_msgs[-1], '(RFC822)')
        email_message = email.message_from_bytes(data[0][1])

        for part in email_message.walk():
            if part.get_content_type() == 'text/plain':
                yield part.get_payload()

I yield the email contents (to be iterated over in the outer scope). I also added a custom exception to allow handling the short time-out in main.

It seems to me like you did one request too many. After your first request you already have a list of all unseen emails, the last of which is the latest email. So there should be no need to do another request here.

Lastly, I re-ordered the logic, so that if there are no new messages, no further requests are made.


The last function is a main function, which calls all the other functions. It is executed in a if __name__ == "__main__": guard to allow importing your code from other scripts:

def main():
    while True:
        try:
            r = RUAlerts.login()
            mail = mail_login()
            try:
                for alert in search_for_alerts(mail):
                    post_alert(alert, r)
            except ShortTimeOut:
                log('All clear on the RU front')
                time.sleep(30)

        except Exception as e:
            log('{} Retrying in 5 minutes'.format(e))
            time.sleep(300)


if __name__ == "__main__":
    main()

Final code:

import praw
import imaplib
import email
import time
import getpass
import RUAlerts
from datetime import datetime

class ShortTimeOut(Exception):
    pass

app_id = 'xxxxxxxxxxxx'
app_secret = 'xxxxxxxxxxxx'
app_uri = 'xxxxxxxxxxxx'
app_scopes = 'account creddits edit flair history identity mysubreddits privatemessages read report save submit subscribe vote'
app_ua = 'xxxxxxxxxxxx'
app_account_code = 'xxxxxxxxxxxx'
app_refresh = 'xxxxxxxxxxxx'


def login():
    r = praw.Reddit(app_ua)
    r.set_oauth_app_info(app_id, app_secret, app_uri)
    r.refresh_access_information(app_refresh)
    return r


def log(text):
    print('\t{:%Y-%m-%d %H:%M:%S} - {}'.format(datetime.now(), text))


def mail_login():
    mail = imaplib.IMAP4_SSL('imap.gmail.com')
    while True:
        try:
            emailpass = getpass.getpass(
                'Please enter the password for xxxxxxx@xxxxx: ')
            mail.login('xxxxxxxxxxxx@gmail.com', emailpass)
            break
        except imaplib.IMAP4.error:
            print('Incorrect password')
    mail.select("inbox")
    return mail


def post_alert(alert, r):
    alert_text = """
    {}

     ********

     *^^I ^^am ^^a ^^bot. ^^For ^^any ^^questions, ^^comments, ^^or ^^concerns, ^^please ^^email [^^xxxxxxx@xxxxx](mailto://xxxxxxx@xxxxx)*
     """
    while True:
        try:
            r.submit(subreddit='xxxxxxxxxxxx', title=alert,
                     text=alert_text.format(alert))
            print('\t{}'.format(alert), end=' ')
            break
        except praw.errors.ExceptionList as e:
            log('Reddit error! {}'.format(e))
            time.sleep(300)



def search_for_alerts(mail):
    result, response = mail.uid('search', None, "(UNSEEN)")
    unread_msgs = response[0].split()

    if not unread_msgs:
        raise ShortTimeOut
    else:
        log('Something\'s wrong!')
        result, data = mail.uid('fetch', unread_msgs[-1], '(RFC822)')
        email_message = email.message_from_bytes(data[0][1])

        for part in email_message.walk():
            if part.get_content_type() == 'text/plain':
                yield part.get_payload()

def main():
    while True:
        try:
            r = RUAlerts.login()
            mail = mail_login()
            try:
                for alert in search_for_alerts(mail):
                    post_alert(alert, r)
            except ShortTimeOut:
                log('All clear on the RU front')
                time.sleep(30)

        except Exception as e:
            log("{}! Retrying in 5 minutes".format(e))
            time.sleep(300)

if __name__ == "__main__":
    main()
\$\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.