-
-
Save thomaswieland/bfbd9e02bc97408a02e6d6c24ac22220 to your computer and use it in GitHub Desktop.
Python script for monitoring an IMAP folder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Each existing unread and subsequent new emails after the script is started are | |
passed as Mail objects to "process_email" function.Function header is provided | |
but processing implementation is left to the user. Error logs are currently sent | |
to a rotating log file (in the same directory as the script) and to STDOUT. | |
Instead of polling or checking the server for new emails every now and then, | |
IMAP IDLE check is utilized. Ensure that the IMAP server supports IDLE command | |
and allows at least 5 minutes of idling*** and uses the default ports for this | |
script to work. Tested to work with Gmail and default installations of MS | |
Exchange Server. | |
Dependencies: | |
Python 2.5+ | |
eventlet | |
Files: | |
1. imap_monitor.py - the script itself | |
2. imap_monitor.ini - a sample INI file with the minimum required sections | |
and settings; must be named as such ("imap_monitor.ini") and be located | |
in the same directory as the script | |
To Do: | |
1. ***Remove hard-coded IDLE timeout; place in config file | |
2. Support non-standard port numbers through config file | |
3. Accept command line settings - control verbosity of logging | |
4. Support SMTP log handling for CRITICAL errors | |
5. Improve/expand error handling blocks |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[imap] | |
host = mail.server.domain | |
username = ArthurCharlesHerbertRuncieMacAdamJarrett | |
password = correcthorsebatterystaple | |
ssl = True | |
folder = INBOX | |
[path] | |
download = /absolute/path/to/download/directory |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import eventlet | |
imapclient = eventlet.import_patched('imapclient') | |
import os.path as path | |
import sys | |
import traceback | |
import logging | |
from logging.handlers import RotatingFileHandler | |
import ConfigParser | |
import email | |
from time import sleep | |
from datetime import datetime, time | |
# Setup the log handlers to stdout and file. | |
log = logging.getLogger('imap_monitor') | |
log.setLevel(logging.DEBUG) | |
formatter = logging.Formatter( | |
'%(asctime)s | %(name)s | %(levelname)s | %(message)s' | |
) | |
handler_stdout = logging.StreamHandler(sys.stdout) | |
handler_stdout.setLevel(logging.DEBUG) | |
handler_stdout.setFormatter(formatter) | |
log.addHandler(handler_stdout) | |
handler_file = RotatingFileHandler( | |
'imap_monitor.log', | |
mode='a', | |
maxBytes=1048576, | |
backupCount=9, | |
encoding='UTF-8', | |
delay=True | |
) | |
handler_file.setLevel(logging.DEBUG) | |
handler_file.setFormatter(formatter) | |
log.addHandler(handler_file) | |
# TODO: Support SMTP log handling for CRITICAL errors. | |
def process_email(mail_, download_, log_): | |
"""Email processing to be done here. mail_ is the Mail object passed to this | |
function. download_ is the path where attachments may be downloaded to. | |
log_ is the logger object. | |
""" | |
log_.info(mail_['subject']) | |
return 'return meaningful result here' | |
def main(): | |
log.info('... script started') | |
while True: | |
# <--- Start of configuration section | |
# Read config file - halt script on failure | |
try: | |
config_file = open('imap_monitor.ini','r+') | |
except IOError: | |
log.critical('configuration file is missing') | |
break | |
config = ConfigParser.SafeConfigParser() | |
config.readfp(config_file) | |
# Retrieve IMAP host - halt script if section 'imap' or value | |
# missing | |
try: | |
host = config.get('imap', 'host') | |
except ConfigParser.NoSectionError: | |
log.critical('no "imap" section in configuration file') | |
break | |
except ConfigParser.NoOptionError: | |
log.critical('no IMAP host specified in configuration file') | |
break | |
# Retrieve IMAP username - halt script if missing | |
try: | |
username = config.get('imap', 'username') | |
except ConfigParser.NoOptionError: | |
log.critical('no IMAP username specified in configuration file') | |
break | |
# Retrieve IMAP password - halt script if missing | |
try: | |
password = config.get('imap', 'password') | |
except ConfigParser.NoOptionError: | |
log.critical('no IMAP password specified in configuration file') | |
break | |
# Retrieve IMAP SSL setting - warn if missing, halt if not boolean | |
try: | |
ssl = config.getboolean('imap', 'ssl') | |
except ConfigParser.NoOptionError: | |
# Default SSL setting to False if missing | |
log.warning('no IMAP SSL setting specified in configuration file') | |
ssl = False | |
except ValueError: | |
log.critical('IMAP SSL setting invalid - not boolean') | |
break | |
# Retrieve IMAP folder to monitor - warn if missing | |
try: | |
folder = config.get('imap', 'folder') | |
except ConfigParser.NoOptionError: | |
# Default folder to monitor to 'INBOX' if missing | |
log.warning('no IMAP folder specified in configuration file') | |
folder = 'INBOX' | |
# Retrieve path for downloads - halt if section of value missing | |
try: | |
download = config.get('path', 'download') | |
except ConfigParser.NoSectionError: | |
log.critical('no "path" section in configuration') | |
break | |
except ConfigParser.NoOptionError: | |
# If value is None or specified path not existing, warn and default | |
# to script path | |
log.warn('no download path specified in configuration') | |
download = None | |
finally: | |
download = download if ( | |
download and path.exists(download) | |
) else path.abspath(__file__) | |
log.info('setting path for email downloads - {0}'.format(download)) | |
while True: | |
# <--- Start of IMAP server connection loop | |
# Attempt connection to IMAP server | |
log.info('connecting to IMAP server - {0}'.format(host)) | |
try: | |
imap = imapclient.IMAPClient(host, use_uid=True, ssl=ssl) | |
except Exception: | |
# If connection attempt to IMAP server fails, retry | |
etype, evalue = sys.exc_info()[:2] | |
estr = traceback.format_exception_only(etype, evalue) | |
logstr = 'failed to connect to IMAP server - ' | |
for each in estr: | |
logstr += '{0}; '.format(each.strip('\n')) | |
log.error(logstr) | |
sleep(10) | |
continue | |
log.info('server connection established') | |
# Attempt login to IMAP server | |
log.info('logging in to IMAP server - {0}'.format(username)) | |
try: | |
result = imap.login(username, password) | |
log.info('login successful - {0}'.format(result)) | |
except Exception: | |
# Halt script when login fails | |
etype, evalue = sys.exc_info()[:2] | |
estr = traceback.format_exception_only(etype, evalue) | |
logstr = 'failed to login to IMAP server - ' | |
for each in estr: | |
logstr += '{0}; '.format(each.strip('\n')) | |
log.critical(logstr) | |
break | |
# Select IMAP folder to monitor | |
log.info('selecting IMAP folder - {0}'.format(folder)) | |
try: | |
result = imap.select_folder(folder) | |
log.info('folder selected') | |
except Exception: | |
# Halt script when folder selection fails | |
etype, evalue = sys.exc_info()[:2] | |
estr = traceback.format_exception_only(etype, evalue) | |
logstr = 'failed to select IMAP folder - ' | |
for each in estr: | |
logstr += '{0}; '.format(each.strip('\n')) | |
log.critical(logstr) | |
break | |
# Retrieve and process all unread messages. Should errors occur due | |
# to loss of connection, attempt restablishing connection | |
try: | |
result = imap.search('UNSEEN') | |
except Exception: | |
continue | |
log.info('{0} unread messages seen - {1}'.format( | |
len(result), result | |
)) | |
for each in result: | |
try: | |
result = imap.fetch(each, ['RFC822']) | |
except Exception: | |
log.error('failed to fetch email - {0}'.format(each)) | |
continue | |
mail = email.message_from_string(result[each]['RFC822']) | |
try: | |
process_email(mail, download, log) | |
log.info('processing email {0} - {1}'.format( | |
each, mail['subject'] | |
)) | |
except Exception: | |
log.error('failed to process email {0}'.format(each)) | |
raise | |
continue | |
while True: | |
# <--- Start of mail monitoring loop | |
# After all unread emails are cleared on initial login, start | |
# monitoring the folder for new email arrivals and process | |
# accordingly. Use the IDLE check combined with occassional NOOP | |
# to refresh. Should errors occur in this loop (due to loss of | |
# connection), return control to IMAP server connection loop to | |
# attempt restablishing connection instead of halting script. | |
imap.idle() | |
# TODO: Remove hard-coded IDLE timeout; place in config file | |
result = imap.idle_check(5*60) | |
if result: | |
imap.idle_done() | |
result = imap.search('UNSEEN') | |
log.info('{0} new unread messages - {1}'.format( | |
len(result),result | |
)) | |
for each in result: | |
fetch = imap.fetch(each, ['RFC822']) | |
mail = email.message_from_string( | |
fetch[each]['RFC822'] | |
) | |
try: | |
process_email(mail, download, log) | |
log.info('processing email {0} - {1}'.format( | |
each, mail['subject'] | |
)) | |
except Exception: | |
log.error( | |
'failed to process email {0}'.format(each)) | |
raise | |
continue | |
else: | |
imap.idle_done() | |
imap.noop() | |
log.info('no new messages seen') | |
# End of mail monitoring loop ---> | |
continue | |
# End of IMAP server connection loop ---> | |
break | |
# End of configuration section ---> | |
break | |
log.info('script stopped ...') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment